From fb3e8f4498680d1d3bc52990572e6d0f0c00c7c2 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 24 Jun 2021 12:43:26 -0700 Subject: [PATCH] [Enterprise Search] Product 404 polish pass (#103198) * Refactor NotFound component - shared NotFound becomes NotFoundPrompt - returns only an EuiEmptyPrompt, and individual products/plugins are in charge of their own layout, rather than NotFound doing a bunch of arduous switch handling (also closer to how errorConnecting is a component set per-plugin) - This is both due to the recent page template refactor and the fact that WS has extra complex logic of needing to switch between its kibana layout and personal dashboard layout - logos are still hosted in shared/ since they need extra custom CSS to work correctly sizing wise and in dark mode. I renamed its folder from `assets`->`logos` for extra clarity * [AS] Update current AS routers using NotFound + update EngineRouter to use NotFound * [WS] Update app router - Handle errorConnecting at the topmost level, instead of in WorkplaceSearchConfigured (to simplify various logic/expectations & match App Search) - Simplify isOrganization check to use `useRouteMatch` instead of a regex - Use new NotFound component - Add NotFound component for the personal dashboard router * [WS] Improve Source 404 UX - Add NotFound to SourceRouter + add breadcrumbs for organization views - When an actual source ID 404s, fix blanket redirect to a dashboard aware redirect - personal dashboard 404s should send the user back to personal sources, not organization sources + add a flash message error (similar to how App Search behaves for engine 404s) + harden error status checks (gracefully allow for non-http errors to fall back flashAPIErrors * [WS] Improve Settings 404 UX - This was the only remaining WS route I found that either did not have a 404 or a fallback to some overview page, so I tweaked the redirect order for a graceful redirect (vs a blank page) * Fix settings router test * Move away from custom product logos to OOTB Enterprise Search logo Keeping it simple, etc. RIP in peace fancy logos * [PR feedback] toContain over stringContaining --- .../components/analytics/analytics_router.tsx | 8 +- .../components/engine/engine_router.tsx | 6 +- .../components/not_found/index.ts} | 16 +-- .../components/not_found/not_found.test.tsx | 38 ++++++ .../components/not_found/not_found.tsx | 23 ++++ .../applications/app_search/index.test.tsx | 17 +-- .../public/applications/app_search/index.tsx | 13 +- .../not_found/assets/app_search_logo.tsx | 33 ----- .../assets/workplace_search_logo.tsx | 39 ------ .../applications/shared/not_found/index.ts | 2 +- .../shared/not_found/not_found.test.tsx | 70 ----------- .../shared/not_found/not_found.tsx | 117 ------------------ .../not_found/not_found_prompt.test.tsx | 51 ++++++++ .../shared/not_found/not_found_prompt.tsx | 65 ++++++++++ .../workplace_search/index.test.tsx | 32 ++--- .../applications/workplace_search/index.tsx | 38 +++--- .../content_sources/source_logic.test.ts | 80 ++++++------ .../views/content_sources/source_logic.ts | 12 +- .../content_sources/source_router.test.tsx | 4 +- .../views/content_sources/source_router.tsx | 6 +- .../workplace_search/views/not_found/index.ts | 8 ++ .../views/not_found/not_found.test.tsx | 52 ++++++++ .../views/not_found/not_found.tsx | 33 +++++ .../views/settings/settings_router.test.tsx | 4 +- .../views/settings/settings_router.tsx | 5 +- 25 files changed, 378 insertions(+), 394 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/{shared/not_found/assets/logo.scss => app_search/components/not_found/index.ts} (53%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx index d56fe949431c3..2ed06d68301c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx @@ -8,8 +8,6 @@ import React from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; -import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { NotFound } from '../../../shared/not_found'; import { ENGINE_ANALYTICS_PATH, ENGINE_ANALYTICS_TOP_QUERIES_PATH, @@ -21,6 +19,7 @@ import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH, } from '../../routes'; import { generateEnginePath, getEngineBreadcrumbs } from '../engine'; +import { NotFound } from '../not_found'; import { ANALYTICS_TITLE } from './constants'; import { @@ -61,10 +60,7 @@ export const AnalyticsRouter: React.FC = () => { - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index da8dd8467bb61..2d1bd32a0fff5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -38,6 +38,7 @@ import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { EngineOverview } from '../engine_overview'; import { AppSearchPageTemplate } from '../layout'; +import { NotFound } from '../not_found'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; import { SchemaRouter } from '../schema'; @@ -45,7 +46,7 @@ import { SearchUI } from '../search_ui'; import { SourceEngines } from '../source_engines'; import { Synonyms } from '../synonyms'; -import { EngineLogic } from './'; +import { EngineLogic, getEngineBreadcrumbs } from './'; export const EngineRouter: React.FC = () => { const { @@ -159,6 +160,9 @@ export const EngineRouter: React.FC = () => { )} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts similarity index 53% rename from x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts index b157f55cbba68..482c1a58faa9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/logo.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/index.ts @@ -5,18 +5,4 @@ * 2.0. */ -.logo404 { - width: $euiSize * 8; - height: $euiSize * 8; - - fill: $euiColorEmptyShade; - stroke: $euiColorLightShade; - - &__light { - fill: $euiColorLightShade; - } - - &__dark { - fill: $euiColorMediumShade; - } -} +export { NotFound } from './not_found'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx new file mode 100644 index 0000000000000..6fed726eb5e0b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.test.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendAppSearchTelemetry } from '../../../shared/telemetry'; +import { AppSearchPageTemplate } from '../layout'; + +import { NotFound } from './'; + +describe('NotFound', () => { + const wrapper = shallow(); + + it('renders the shared not found prompt', () => { + expect(wrapper.find(NotFoundPrompt)).toHaveLength(1); + }); + + it('renders a telemetry error event', () => { + expect(wrapper.find(SendAppSearchTelemetry).prop('action')).toEqual('error'); + }); + + it('passes optional preceding page chrome', () => { + wrapper.setProps({ pageChrome: ['Engines', 'some-engine'] }); + + expect(wrapper.find(AppSearchPageTemplate).prop('pageChrome')).toEqual([ + 'Engines', + 'some-engine', + '404', + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx new file mode 100644 index 0000000000000..f6165fa192d57 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/not_found/not_found.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { PageTemplateProps } from '../../../shared/layout'; +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendAppSearchTelemetry } from '../../../shared/telemetry'; +import { AppSearchPageTemplate } from '../layout'; + +export const NotFound: React.FC = ({ pageChrome = [] }) => { + return ( + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 00acea945177a..46596cc5d6765 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -17,7 +17,7 @@ import { Redirect } from 'react-router-dom'; import { shallow, ShallowWrapper } from 'enzyme'; -import { Layout, SideNav, SideNavLink } from '../shared/layout'; +import { SideNav, SideNavLink } from '../shared/layout'; import { rerender } from '../test_helpers'; @@ -83,13 +83,6 @@ describe('AppSearchConfigured', () => { wrapper = shallow(); }); - it('renders with layout', () => { - expect(wrapper.find(Layout)).toHaveLength(1); - expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy(); - expect(wrapper.find(EnginesOverview)).toHaveLength(1); - expect(wrapper.find(EngineRouter)).toHaveLength(1); - }); - it('renders header actions', () => { expect(renderHeaderActions).toHaveBeenCalled(); }); @@ -98,11 +91,9 @@ describe('AppSearchConfigured', () => { expect(AppLogic).toHaveBeenCalledWith(DEFAULT_INITIAL_APP_DATA); }); - it('passes readOnlyMode state', () => { - setMockValues({ myRole: {}, readOnlyMode: true }); - rerender(wrapper); - - expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); + it('renders engine routes', () => { + expect(wrapper.find(EnginesOverview)).toHaveLength(1); + expect(wrapper.find(EngineRouter)).toHaveLength(1); }); describe('routes with ability checks', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index d7ddad5683f38..6d049b2015487 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -14,8 +14,7 @@ import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; import { KibanaLogic } from '../shared/kibana'; -import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { NotFound } from '../shared/not_found'; +import { SideNav, SideNavLink } from '../shared/layout'; import { ROLE_MAPPINGS_TITLE } from '../shared/role_mapping/constants'; @@ -28,6 +27,7 @@ import { ErrorConnecting } from './components/error_connecting'; import { KibanaHeaderActions } from './components/layout'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; +import { NotFound } from './components/not_found'; import { RoleMappings } from './components/role_mappings'; import { Settings, SETTINGS_TITLE } from './components/settings'; import { SetupGuide } from './components/setup_guide'; @@ -85,7 +85,6 @@ export const AppSearchConfigured: React.FC> = (props) = }, } = useValues(AppLogic(props)); const { renderHeaderActions } = useValues(KibanaLogic); - const { readOnlyMode } = useValues(HttpLogic); useEffect(() => { renderHeaderActions(KibanaHeaderActions); @@ -133,13 +132,7 @@ export const AppSearchConfigured: React.FC> = (props) = )} - } readOnlyMode={readOnlyMode}> - - - - - - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx deleted file mode 100644 index 8eb2059afd3ed..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/app_search_logo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -export const AppSearchLogo: React.FC = () => ( - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx deleted file mode 100644 index df5b1a1118c41..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/assets/workplace_search_logo.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -export const WorkplaceSearchLogo: React.FC = () => ( - -); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts index 482c1a58faa9c..8be374d549952 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { NotFound } from './not_found'; +export { NotFoundPrompt } from './not_found_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx deleted file mode 100644 index 1561224a26e42..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { setMockValues } from '../../__mocks__/kea_logic'; - -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; - -import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; -import { SetAppSearchChrome } from '../kibana_chrome'; - -import { AppSearchLogo } from './assets/app_search_logo'; -import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; - -import { NotFound } from './'; - -describe('NotFound', () => { - it('renders an App Search 404 view', () => { - const wrapper = shallow(); - const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); - - expect(prompt.find('h2').text()).toEqual('404 error'); - expect(prompt.find(EuiButtonExternal).prop('href')).toEqual(APP_SEARCH_PLUGIN.SUPPORT_URL); - - const logo = prompt.find(AppSearchLogo).dive().shallow(); - expect(logo.type()).toEqual('svg'); - }); - - it('renders a Workplace Search 404 view', () => { - const wrapper = shallow(); - const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); - - expect(prompt.find('h2').text()).toEqual('404 error'); - expect(prompt.find(EuiButtonExternal).prop('href')).toEqual( - WORKPLACE_SEARCH_PLUGIN.SUPPORT_URL - ); - - const logo = prompt.find(WorkplaceSearchLogo).dive().shallow(); - expect(logo.type()).toEqual('svg'); - }); - - it('changes the support URL if the user has a gold+ license', () => { - setMockValues({ hasGoldLicense: true }); - const wrapper = shallow(); - const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); - - expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co'); - }); - - it('passes down optional custom breadcrumbs', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(SetAppSearchChrome).prop('trail')).toEqual(['Hello', 'World']); - }); - - it('does not render anything without a valid product', () => { - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx deleted file mode 100644 index f288961b72de4..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useValues } from 'kea'; - -import { - EuiPageContent, - EuiEmptyPrompt, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { - APP_SEARCH_PLUGIN, - WORKPLACE_SEARCH_PLUGIN, - LICENSED_SUPPORT_URL, -} from '../../../../common/constants'; - -import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; -import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs'; -import { LicensingLogic } from '../licensing'; -import { EuiButtonTo } from '../react_router_helpers'; -import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; - -import { AppSearchLogo } from './assets/app_search_logo'; -import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; -import './assets/logo.scss'; - -interface NotFoundProps { - // Expects product plugin constants (@see common/constants.ts) - product: { - ID: string; - SUPPORT_URL: string; - }; - // Optional breadcrumbs - breadcrumbs?: BreadcrumbTrail; -} - -export const NotFound: React.FC = ({ product = {}, breadcrumbs }) => { - const { hasGoldLicense } = useValues(LicensingLogic); - const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; - - let Logo; - let SetPageChrome; - let SendTelemetry; - - switch (product.ID) { - case APP_SEARCH_PLUGIN.ID: - Logo = AppSearchLogo; - SetPageChrome = SetAppSearchChrome; - SendTelemetry = SendAppSearchTelemetry; - break; - case WORKPLACE_SEARCH_PLUGIN.ID: - Logo = WorkplaceSearchLogo; - SetPageChrome = SetWorkplaceSearchChrome; - SendTelemetry = SendWorkplaceSearchTelemetry; - break; - default: - return null; - } - - return ( - <> - - - - - } - body={ - <> - -

- {i18n.translate('xpack.enterpriseSearch.notFound.title', { - defaultMessage: '404 error', - })} -

-
-

- {i18n.translate('xpack.enterpriseSearch.notFound.description', { - defaultMessage: 'The page you’re looking for was not found.', - })} -

- - } - actions={ - - - - {i18n.translate('xpack.enterpriseSearch.notFound.action1', { - defaultMessage: 'Back to your dashboard', - })} - - - - - {i18n.translate('xpack.enterpriseSearch.notFound.action2', { - defaultMessage: 'Contact support', - })} - - - - } - /> -
- - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx new file mode 100644 index 0000000000000..c21aeff2780b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; + +import { EuiButtonTo } from '../react_router_helpers'; + +import { NotFoundPrompt } from './'; + +describe('NotFoundPrompt', () => { + const subject = (props?: object) => + shallow() + .find(EuiEmptyPrompt) + .dive(); + + it('renders', () => { + const wrapper = subject({ + productSupportUrl: 'https://discuss.elastic.co/c/enterprise-search/app-search/', + }); + + expect(wrapper.find('h1').text()).toEqual('404 error'); + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/'); + expect(wrapper.find(EuiButton).prop('href')).toContain('https://discuss.elastic.co'); + }); + + it('renders with a custom "Back to dashboard" link if passed', () => { + const wrapper = subject({ + productSupportUrl: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/', + backToLink: '/workplace_search/p/sources', + }); + + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual('/workplace_search/p/sources'); + }); + + it('renders with a link to our licensed support URL for gold+ licenses', () => { + setMockValues({ hasGoldLicense: true }); + const wrapper = subject(); + + expect(wrapper.find(EuiButton).prop('href')).toEqual('https://support.elastic.co'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx new file mode 100644 index 0000000000000..97debd21ec16c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found_prompt.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { LICENSED_SUPPORT_URL } from '../../../../common/constants'; +import { LicensingLogic } from '../licensing'; +import { EuiButtonTo } from '../react_router_helpers'; + +interface Props { + productSupportUrl: string; + backToLink?: string; +} + +export const NotFoundPrompt: React.FC = ({ productSupportUrl, backToLink = '/' }) => { + const { hasGoldLicense } = useValues(LicensingLogic); + const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : productSupportUrl; + + return ( + + {i18n.translate('xpack.enterpriseSearch.notFound.title', { + defaultMessage: '404 error', + })} + + } + body={ +

+ {i18n.translate('xpack.enterpriseSearch.notFound.description', { + defaultMessage: 'The page you’re looking for was not found.', + })} +

+ } + actions={ + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action1', { + defaultMessage: 'Back to your dashboard', + })} + + + + + {i18n.translate('xpack.enterpriseSearch.notFound.action2', { + defaultMessage: 'Contact support', + })} + + + + } + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 28169afd4bdeb..2743dfc794ec6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -5,17 +5,15 @@ * 2.0. */ -import '../__mocks__/react_router'; import '../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions, mockKibanaValues } from '../__mocks__/kea_logic'; +import { mockUseRouteMatch } from '../__mocks__/react_router'; import React from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { Layout } from '../shared/layout'; - import { WorkplaceSearchHeaderActions } from './components/layout'; import { SourceAdded } from './views/content_sources/components/source_added'; import { ErrorState } from './views/error_state'; @@ -38,6 +36,14 @@ describe('WorkplaceSearch', () => { expect(wrapper.find(WorkplaceSearchConfigured)).toHaveLength(1); }); + + it('renders ErrorState', () => { + setMockValues({ errorConnecting: true }); + + const wrapper = shallow(); + + expect(wrapper.find(ErrorState)).toHaveLength(1); + }); }); describe('WorkplaceSearchUnconfigured', () => { @@ -56,12 +62,12 @@ describe('WorkplaceSearchConfigured', () => { beforeEach(() => { jest.clearAllMocks(); setMockActions({ initializeAppData, setContext }); + mockUseRouteMatch.mockReturnValue(false); }); - it('renders layout, chrome, and header actions', () => { + it('renders chrome and header actions', () => { const wrapper = shallow(); - expect(wrapper.find(Layout).first().prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(Overview)).toHaveLength(1); expect(mockKibanaValues.setChromeIsVisible).toHaveBeenCalledWith(true); @@ -83,22 +89,6 @@ describe('WorkplaceSearchConfigured', () => { expect(mockKibanaValues.renderHeaderActions).not.toHaveBeenCalled(); }); - it('renders ErrorState', () => { - setMockValues({ errorConnecting: true }); - - const wrapper = shallow(); - - expect(wrapper.find(ErrorState)).toHaveLength(1); - }); - - it('passes readOnlyMode state', () => { - setMockValues({ readOnlyMode: true }); - - const wrapper = shallow(); - - expect(wrapper.find(Layout).first().prop('readOnlyMode')).toEqual(true); - }); - it('renders SourceAdded', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 05018be2934b4..2daf677962163 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -6,19 +6,16 @@ */ import React, { useEffect } from 'react'; -import { Route, Redirect, Switch, useLocation } from 'react-router-dom'; +import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; import { HttpLogic } from '../shared/http'; import { KibanaLogic } from '../shared/kibana'; -import { Layout } from '../shared/layout'; -import { NotFound } from '../shared/not_found'; import { AppLogic } from './app_logic'; -import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; +import { WorkplaceSearchHeaderActions } from './components/layout'; import { GROUPS_PATH, SETUP_GUIDE_PATH, @@ -36,6 +33,7 @@ import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; import { ErrorState } from './views/error_state'; import { GroupsRouter } from './views/groups'; +import { NotFound } from './views/not_found'; import { Overview } from './views/overview'; import { RoleMappings } from './views/role_mappings'; import { Security } from './views/security'; @@ -44,30 +42,33 @@ import { SetupGuide } from './views/setup_guide'; export const WorkplaceSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); - return !config.host ? : ; + const { errorConnecting } = useValues(HttpLogic); + return !config.host ? ( + + ) : errorConnecting ? ( + + ) : ( + + ); }; export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData, setContext } = useActions(AppLogic); const { renderHeaderActions, setChromeIsVisible } = useValues(KibanaLogic); - const { errorConnecting, readOnlyMode } = useValues(HttpLogic); - - const { pathname } = useLocation(); /** * Personal dashboard urls begin with /p/ * EX: http://localhost:5601/app/enterprise_search/workplace_search/p/sources */ - const personalSourceUrlRegex = /^\/p\//g; // matches '/p/*' - const isOrganization = !pathname.match(personalSourceUrlRegex); // TODO: Once auth is figured out, we need to have a check for the equivilent of `isAdmin`. + const isOrganization = !useRouteMatch(PERSONAL_PATH); // TODO: Once auth is figured out, we need to have a check for the equivalent of `isAdmin`. setContext(isOrganization); useEffect(() => { setChromeIsVisible(isOrganization); - }, [pathname]); + }, [isOrganization]); useEffect(() => { if (!hasInitialized) { @@ -95,6 +96,9 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + + @@ -113,15 +117,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { - } restrictWidth readOnlyMode={readOnlyMode}> - {errorConnecting ? ( - - ) : ( - - - - )} - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index 03f46830fafc3..2aed64af53f16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -22,8 +22,6 @@ jest.mock('../../app_logic', () => ({ })); import { AppLogic } from '../../app_logic'; -import { NOT_FOUND_PATH } from '../../routes'; - import { SourceLogic } from './source_logic'; describe('SourceLogic', () => { @@ -176,47 +174,55 @@ describe('SourceLogic', () => { expect(initializeFederatedSummarySpy).toHaveBeenCalledWith(contentSource.id); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); - SourceLogic.actions.initializeSource(contentSource.id); - await expectedAsyncError(promise); + describe('errors', () => { + it('handles generic errors', async () => { + const mockError = Promise.reject('error'); + http.get.mockReturnValue(mockError); - expect(flashAPIErrors).toHaveBeenCalledWith(error); - }); + SourceLogic.actions.initializeSource(contentSource.id); + await expectedAsyncError(mockError); - it('handles not found state', async () => { - const error = { - response: { - error: 'this is an error', - status: 404, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); - SourceLogic.actions.initializeSource(contentSource.id); - await expectedAsyncError(promise); + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); - expect(navigateToUrl).toHaveBeenCalledWith(NOT_FOUND_PATH); - }); + describe('404s', () => { + const mock404 = Promise.reject({ response: { status: 404 } }); - it('renders error messages passed in success response from server', async () => { - const errors = ['ERROR']; - const promise = Promise.resolve({ - ...contentSource, - errors, + it('redirects to the organization sources page on organization views', async () => { + AppLogic.values.isOrganization = true; + http.get.mockReturnValue(mock404); + + SourceLogic.actions.initializeSource('404ing_org_source'); + await expectedAsyncError(mock404); + + expect(navigateToUrl).toHaveBeenCalledWith('/sources'); + expect(setErrorMessage).toHaveBeenCalledWith('Source not found.'); + }); + + it('redirects to the personal dashboard sources page on personal views', async () => { + AppLogic.values.isOrganization = false; + http.get.mockReturnValue(mock404); + + SourceLogic.actions.initializeSource('404ing_personal_source'); + await expectedAsyncError(mock404); + + expect(navigateToUrl).toHaveBeenCalledWith('/p/sources'); + expect(setErrorMessage).toHaveBeenCalledWith('Source not found.'); + }); }); - http.get.mockReturnValue(promise); - SourceLogic.actions.initializeSource(contentSource.id); - await promise; - expect(setErrorMessage).toHaveBeenCalledWith(errors); + it('renders error messages passed in success response from server', async () => { + const errors = ['ERROR']; + const promise = Promise.resolve({ + ...contentSource, + errors, + }); + http.get.mockReturnValue(promise); + SourceLogic.actions.initializeSource(contentSource.id); + await promise; + + expect(setErrorMessage).toHaveBeenCalledWith(errors); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index 2e6a3c65597ea..0fd44e01ae495 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -20,7 +20,7 @@ import { import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; import { AppLogic } from '../../app_logic'; -import { NOT_FOUND_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; +import { PERSONAL_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; import { ContentSourceFullData, Meta, DocumentSummaryItem, SourceContentItem } from '../../types'; export interface SourceActions { @@ -155,8 +155,14 @@ export const SourceLogic = kea>({ clearFlashMessages(); } } catch (e) { - if (e.response.status === 404) { - KibanaLogic.values.navigateToUrl(NOT_FOUND_PATH); + if (e?.response?.status === 404) { + const redirect = isOrganization ? SOURCES_PATH : PERSONAL_SOURCES_PATH; + KibanaLogic.values.navigateToUrl(redirect); + setErrorMessage( + i18n.translate('xpack.enterpriseSearch.workplaceSearch.sources.notFoundErrorMessage', { + defaultMessage: 'Source not found.', + }) + ); } else { flashAPIErrors(e); } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index afe0d1f89faea..fbc8eb159a7a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -90,7 +90,7 @@ describe('SourceRouter', () => { expect(wrapper.find(Overview)).toHaveLength(1); expect(wrapper.find(SourceSettings)).toHaveLength(1); expect(wrapper.find(SourceContent)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(3); + expect(wrapper.find(Route)).toHaveLength(4); }); it('renders source routes (custom)', () => { @@ -100,6 +100,6 @@ describe('SourceRouter', () => { expect(wrapper.find(DisplaySettingsRouter)).toHaveLength(1); expect(wrapper.find(Schema)).toHaveLength(1); expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1); - expect(wrapper.find(Route)).toHaveLength(6); + expect(wrapper.find(Route)).toHaveLength(7); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index bf68a60757c0d..9f793fcd34fbe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -13,7 +13,7 @@ import { useActions, useValues } from 'kea'; import { AppLogic } from '../../app_logic'; import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; -import { CUSTOM_SERVICE_TYPE } from '../../constants'; +import { NAV, CUSTOM_SERVICE_TYPE } from '../../constants'; import { REINDEX_JOB_PATH, SOURCE_DETAILS_PATH, @@ -24,6 +24,7 @@ import { getContentSourcePath as sourcePath, getSourcesPath, } from '../../routes'; +import { NotFound } from '../../views/not_found'; import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; @@ -85,6 +86,9 @@ export const SourceRouter: React.FC = () => { + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts new file mode 100644 index 0000000000000..482c1a58faa9c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright 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 { NotFound } from './not_found'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx new file mode 100644 index 0000000000000..0e388a73f0e18 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendWorkplaceSearchTelemetry } from '../../../shared/telemetry'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; + +import { NotFound } from './'; + +describe('NotFound', () => { + it('renders the shared not found prompt', () => { + const wrapper = shallow(); + expect(wrapper.find(NotFoundPrompt)).toHaveLength(1); + }); + + it('renders a telemetry error event', () => { + const wrapper = shallow(); + expect(wrapper.find(SendWorkplaceSearchTelemetry).prop('action')).toEqual('error'); + }); + + it('passes optional preceding page chrome', () => { + const wrapper = shallow(); + expect(wrapper.prop('pageChrome')).toEqual(['Sources', '404']); + }); + + describe('organization views', () => { + it('renders the WorkplaceSearchPageTemplate', () => { + const wrapper = shallow(); + expect(wrapper.type()).toEqual(WorkplaceSearchPageTemplate); + }); + }); + + describe('personal views', () => { + it('renders the PersonalDashboardLayout', () => { + const wrapper = shallow(); + expect(wrapper.type()).toEqual(PersonalDashboardLayout); + }); + + it('sets the "Back to dashboard" link to /p/sources', () => { + const wrapper = shallow(); + expect(wrapper.find(NotFoundPrompt).prop('backToLink')).toEqual('/p/sources'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx new file mode 100644 index 0000000000000..ef55668775513 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/not_found/not_found.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { PageTemplateProps } from '../../../shared/layout'; +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendWorkplaceSearchTelemetry } from '../../../shared/telemetry'; +import { WorkplaceSearchPageTemplate, PersonalDashboardLayout } from '../../components/layout'; +import { PERSONAL_SOURCES_PATH } from '../../routes'; + +interface Props { + isOrganization?: boolean; + pageChrome?: PageTemplateProps['pageChrome']; +} +export const NotFound: React.FC = ({ isOrganization = true, pageChrome = [] }) => { + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + return ( + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx index 74092f17eadcf..123167f0ad1d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx @@ -25,8 +25,8 @@ import { SettingsRouter } from './settings_router'; describe('SettingsRouter', () => { const initializeSettings = jest.fn(); const NUM_SOURCES = staticSourceData.length; - // Should be 3 routes other than the sources listed Connectors, Customize, & OauthApplication - const NUM_ROUTES = NUM_SOURCES + 3; + // Should be 4 routes other than the sources listed: Connectors, Customize, & OauthApplication, & a redirect + const NUM_ROUTES = NUM_SOURCES + 4; beforeEach(() => { setMockActions({ initializeSettings }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx index f8c8050e20153..d9aeba361d240 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx @@ -11,7 +11,6 @@ import { Redirect, Route, Switch } from 'react-router-dom'; import { useActions } from 'kea'; import { - ORG_SETTINGS_PATH, ORG_SETTINGS_CUSTOMIZE_PATH, ORG_SETTINGS_CONNECTORS_PATH, ORG_SETTINGS_OAUTH_APPLICATION_PATH, @@ -33,7 +32,6 @@ export const SettingsRouter: React.FC = () => { return ( - @@ -48,6 +46,9 @@ export const SettingsRouter: React.FC = () => { ))} + + + ); };