diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss index d7740724204a7..529fb9c4bd63e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.scss @@ -15,3 +15,11 @@ margin-top: $euiSizeXS; } } + +.appSearchNavIcons { + // EUI override + &.euiFlexItem { + flex-grow: 0; + flex-direction: row; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index 43dc592660564..4efbf88d1957f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -9,7 +9,7 @@ import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { shallow, mount } from 'enzyme'; +import { shallow } from 'enzyme'; import { Switch, Redirect, useParams } from 'react-router-dom'; import { EuiBadge } from '@elastic/eui'; @@ -82,106 +82,153 @@ describe('EngineRouter', () => { }); describe('EngineNav', () => { + const values = { myRole: {}, engineName: 'some-engine', dataLoading: false, engine: {} }; + beforeEach(() => { - (useParams as jest.Mock).mockReturnValue({ engineName: 'some-engine' }); + setMockValues(values); + }); + + it('does not render if async data is still loading', () => { + setMockValues({ ...values, dataLoading: true }); + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); }); it('does not render without an engine name', () => { - setMockValues({ myRole: {} }); - (useParams as jest.Mock).mockReturnValue({ engineName: '' }); + setMockValues({ ...values, engineName: '' }); const wrapper = shallow(); expect(wrapper.isEmptyRender()).toBe(true); }); - it('renders an engine label', () => { - setMockValues({ myRole: {} }); - const wrapper = mount(); + it('renders an engine label and badges', () => { + setMockValues({ ...values, isSampleEngine: false, isMetaEngine: false }); + const wrapper = shallow(); + const label = wrapper.find('[data-test-subj="EngineLabel"]').find('.eui-textTruncate'); + + expect(label.text()).toEqual('SOME-ENGINE'); + expect(wrapper.find(EuiBadge)).toHaveLength(0); - const label = wrapper.find('[data-test-subj="EngineLabel"]').last(); - expect(label.text()).toEqual(expect.stringContaining('SOME-ENGINE')); + setMockValues({ ...values, isSampleEngine: true }); + wrapper.setProps({}); // Re-render + expect(wrapper.find(EuiBadge).prop('children')).toEqual('SAMPLE ENGINE'); - // TODO: Test sample & meta engine conditional rendering - expect(label.find(EuiBadge).text()).toEqual('SAMPLE ENGINE'); + setMockValues({ ...values, isMetaEngine: true }); + wrapper.setProps({}); // Re-render + expect(wrapper.find(EuiBadge).prop('children')).toEqual('META ENGINE'); }); it('renders a default engine overview link', () => { - setMockValues({ myRole: {} }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineOverviewLink"]')).toHaveLength(1); }); it('renders an analytics link', () => { - setMockValues({ myRole: { canViewEngineAnalytics: true } }); + setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineAnalyticsLink"]')).toHaveLength(1); }); it('renders a documents link', () => { - setMockValues({ myRole: { canViewEngineDocuments: true } }); + setMockValues({ ...values, myRole: { canViewEngineDocuments: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineDocumentsLink"]')).toHaveLength(1); }); it('renders a schema link', () => { - setMockValues({ myRole: { canViewEngineSchema: true } }); + setMockValues({ ...values, myRole: { canViewEngineSchema: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineSchemaLink"]')).toHaveLength(1); + }); + + it('renders schema nav icons', () => { + const myRole = { canViewEngineSchema: true }; + const wrapper = shallow(); + + setMockValues({ ...values, myRole, hasUnconfirmedSchemaFields: true }); + wrapper.setProps({}); // Re-render + expect(wrapper.find('[data-test-subj="EngineNavSchemaUnconfirmedFields"]')).toHaveLength(1); - // TODO: Schema warning icon + setMockValues({ ...values, myRole, hasSchemaConflicts: true }); + wrapper.setProps({}); // Re-render + expect(wrapper.find('[data-test-subj="EngineNavSchemaConflicts"]')).toHaveLength(1); }); - // TODO: Unskip when EngineLogic is migrated - it.skip('renders a crawler link', () => { - setMockValues({ myRole: { canViewEngineCrawler: true } }); + it('renders a crawler link', () => { + const myRole = { canViewEngineCrawler: true }; + setMockValues({ ...values, myRole }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(1); - // TODO: Test that the crawler link does NOT show up for meta/sample engines + // Does not render for meta engines + setMockValues({ ...values, myRole, isMetaEngine: true }); + wrapper.setProps({}); // Re-render + expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(0); + + // Does not render for sample engine + setMockValues({ ...values, myRole, isSampleEngine: true }); + wrapper.setProps({}); // Re-render + expect(wrapper.find('[data-test-subj="EngineCrawlerLink"]')).toHaveLength(0); }); - // TODO: Unskip when EngineLogic is migrated - it.skip('renders a meta engine source engines link', () => { - setMockValues({ myRole: { canViewMetaEngineSourceEngines: true } }); + it('renders a meta engine source engines link', () => { + const myRole = { canViewMetaEngineSourceEngines: true }; + setMockValues({ ...values, myRole, isMetaEngine: true }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(1); - // TODO: Test that the crawler link does NOT show up for non-meta engines + // Does not render if engine is not a meta-engine + setMockValues({ ...values, myRole, isMetaEngine: false }); + wrapper.setProps({}); // Re-render + expect(wrapper.find('[data-test-subj="MetaEngineEnginesLink"]')).toHaveLength(0); }); it('renders a relevance tuning link', () => { - setMockValues({ myRole: { canManageEngineRelevanceTuning: true } }); + setMockValues({ ...values, myRole: { canManageEngineRelevanceTuning: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineRelevanceTuningLink"]')).toHaveLength(1); + }); + + it('renders relevance tuning nav icons', () => { + setMockValues({ + ...values, + myRole: { canManageEngineRelevanceTuning: true }, + engine: { + unsearchedUnconfirmedFields: true, + invalidBoosts: true, + }, + }); + const wrapper = shallow(); - // TODO: Boost error icon + expect(wrapper.find('[data-test-subj="EngineNavRelevanceTuningInvalidBoosts"]')).toHaveLength(1); // prettier-ignore + expect(wrapper.find('[data-test-subj="EngineNavRelevanceTuningUnsearchedFields"]')).toHaveLength(1); // prettier-ignore }); it('renders a synonyms link', () => { - setMockValues({ myRole: { canManageEngineSynonyms: true } }); + setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineSynonymsLink"]')).toHaveLength(1); }); it('renders a curations link', () => { - setMockValues({ myRole: { canManageEngineCurations: true } }); + setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineCurationsLink"]')).toHaveLength(1); }); it('renders a results settings link', () => { - setMockValues({ myRole: { canManageEngineResultSettings: true } }); + setMockValues({ ...values, myRole: { canManageEngineResultSettings: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineResultSettingsLink"]')).toHaveLength(1); }); it('renders a Search UI link', () => { - setMockValues({ myRole: { canManageEngineSearchUi: true } }); + setMockValues({ ...values, myRole: { canManageEngineSearchUi: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineSearchUILink"]')).toHaveLength(1); }); it('renders an API logs link', () => { - setMockValues({ myRole: { canViewEngineApiLogs: true } }); + setMockValues({ ...values, myRole: { canViewEngineApiLogs: true } }); const wrapper = shallow(); expect(wrapper.find('[data-test-subj="EngineAPILogsLink"]')).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 8c03f59702639..9efb0b7804b86 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -8,7 +8,7 @@ import React, { useEffect } from 'react'; import { Route, Switch, Redirect, useParams } from 'react-router-dom'; import { useValues, useActions } from 'kea'; -import { EuiText, EuiBadge } from '@elastic/eui'; +import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SideNavLink, SideNavItem } from '../../../shared/layout'; @@ -48,6 +48,7 @@ import { } from './constants'; import { EngineLogic } from './'; +import { EngineDetails } from './types'; import './engine_nav.scss'; @@ -114,14 +115,22 @@ export const EngineNav: React.FC = () => { }, } = useValues(AppLogic); - // TODO: Use EngineLogic - const isSampleEngine = true; - const isMetaEngine = false; - const { engineName } = useParams() as { engineName: string }; - const engineRoute = engineName && getEngineRoute(engineName); + const { + engineName, + dataLoading, + isSampleEngine, + isMetaEngine, + hasSchemaConflicts, + hasUnconfirmedSchemaFields, + engine, + } = useValues(EngineLogic); + if (dataLoading) return null; if (!engineName) return null; + const engineRoute = getEngineRoute(engineName); + const { invalidBoosts, unsearchedUnconfirmedFields } = engine as Required; + return ( <> @@ -166,8 +175,33 @@ export const EngineNav: React.FC = () => { to={getAppSearchUrl(engineRoute + ENGINE_SCHEMA_PATH)} data-test-subj="EngineSchemaLink" > - {SCHEMA_TITLE} - {/* TODO: Engine schema warning icon */} + + {SCHEMA_TITLE} + + {hasUnconfirmedSchemaFields && ( + + )} + {hasSchemaConflicts && ( + + )} + + )} {canViewEngineCrawler && !isMetaEngine && !isSampleEngine && ( @@ -194,8 +228,33 @@ export const EngineNav: React.FC = () => { to={getAppSearchUrl(engineRoute + ENGINE_RELEVANCE_TUNING_PATH)} data-test-subj="EngineRelevanceTuningLink" > - {RELEVANCE_TUNING_TITLE} - {/* TODO: invalid boosts error icon */} + + {RELEVANCE_TUNING_TITLE} + + {invalidBoosts && ( + + )} + {unsearchedUnconfirmedFields && ( + + )} + + )} {canManageEngineSynonyms && (