From 6d290df30c73f569256208e10f207f0ad283c326 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 1 Jun 2022 15:12:45 +0200 Subject: [PATCH] [ML] Fixes handling of unrecognised URLs (#133157) * not found page * replace not found page with a warning banner * add check for non-empty pathname * functional test * disabled anomaly detection section * update message --- .../components/ml_page/ml_page.tsx | 43 ++++---- .../components/ml_page/side_nav.tsx | 1 + .../application/routing/use_active_route.ts | 53 ---------- .../application/routing/use_active_route.tsx | 99 +++++++++++++++++++ .../apps/ml/permissions/read_ml_access.ts | 10 +- .../functional/services/ml/overview_page.ts | 12 +++ 6 files changed, 144 insertions(+), 74 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/routing/use_active_route.ts create mode 100644 x-pack/plugins/ml/public/application/routing/use_active_route.tsx diff --git a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx index d41ca59255467..7602da6a6c4e3 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/ml_page.tsx @@ -8,7 +8,7 @@ import React, { createContext, FC, useCallback, useMemo, useReducer } from 'react'; import { EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Route } from 'react-router-dom'; +import { Redirect, Route, Switch } from 'react-router-dom'; import type { AppMountParameters } from '@kbn/core/public'; import { KibanaPageTemplate, RedirectAppLinks } from '@kbn/kibana-react-plugin/public'; import { useSideNavItems } from './side_nav'; @@ -137,25 +137,28 @@ const CommonPageWrapper: FC = React.memo( value={{ setPageTitle, setHeaderActionMenu: pageDeps.setHeaderActionMenu }} > - {routeList.map((route) => { - return ( - { - window.setTimeout(() => { - pageDeps.setBreadcrumbs(route.breadcrumbs); - }); - return ( - - {route.render(props, pageDeps)} - - ); - }} - /> - ); - })} + + {routeList.map((route) => { + return ( + { + window.setTimeout(() => { + pageDeps.setBreadcrumbs(route.breadcrumbs); + }); + return ( + + {route.render(props, pageDeps)} + + ); + }} + /> + ); + })} + + diff --git a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx index 90bba94ee2259..9ddee2348ee39 100644 --- a/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_page/side_nav.tsx @@ -86,6 +86,7 @@ export function useSideNavItems(activeRoute: MlRoute | undefined) { name: i18n.translate('xpack.ml.navMenu.anomalyDetectionTabLinkText', { defaultMessage: 'Anomaly Detection', }), + disabled: disableLinks, items: [ { id: 'anomaly_detection', diff --git a/x-pack/plugins/ml/public/application/routing/use_active_route.ts b/x-pack/plugins/ml/public/application/routing/use_active_route.ts deleted file mode 100644 index 9183e45c3d0ae..0000000000000 --- a/x-pack/plugins/ml/public/application/routing/use_active_route.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useLocation, useRouteMatch } from 'react-router-dom'; -import { keyBy } from 'lodash'; -import { useMemo } from 'react'; -import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; -import { useMlKibana } from '../contexts/kibana'; -import type { MlRoute } from './router'; - -/** - * Provides an active route of the ML app. - * @param routesList - */ -export const useActiveRoute = (routesList: MlRoute[]): MlRoute => { - const { pathname } = useLocation(); - - const { - services: { executionContext }, - } = useMlKibana(); - - /** - * Temp fix for routes with params. - */ - const editCalendarMatch = useRouteMatch('/settings/calendars_list/edit_calendar/:calendarId'); - const editFilterMatch = useRouteMatch('/settings/filter_lists/edit_filter_list/:filterId'); - - const routesMap = useMemo(() => keyBy(routesList, 'path'), []); - - const activeRoute = useMemo(() => { - if (editCalendarMatch) { - return routesMap[editCalendarMatch.path]; - } - if (editFilterMatch) { - return routesMap[editFilterMatch.path]; - } - // Remove trailing slash from the pathname - const pathnameKey = pathname.replace(/\/$/, ''); - return routesMap[pathnameKey] ?? routesMap['/overview']; - }, [pathname]); - - useExecutionContext(executionContext, { - name: 'Machine Learning', - type: 'application', - page: activeRoute?.path, - }); - - return activeRoute; -}; diff --git a/x-pack/plugins/ml/public/application/routing/use_active_route.tsx b/x-pack/plugins/ml/public/application/routing/use_active_route.tsx new file mode 100644 index 0000000000000..73651549238a4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/routing/use_active_route.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useLocation, useRouteMatch } from 'react-router-dom'; +import { keyBy } from 'lodash'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { toMountPoint, useExecutionContext } from '@kbn/kibana-react-plugin/public'; +import { EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useMlKibana } from '../contexts/kibana'; +import type { MlRoute } from './router'; + +/** + * Provides an active route of the ML app. + * @param routesList + */ +export const useActiveRoute = (routesList: MlRoute[]): MlRoute => { + const { pathname } = useLocation(); + + const { + services: { executionContext, overlays, theme }, + } = useMlKibana(); + + /** + * Temp fix for routes with params. + */ + const editCalendarMatch = useRouteMatch('/settings/calendars_list/edit_calendar/:calendarId'); + const editFilterMatch = useRouteMatch('/settings/filter_lists/edit_filter_list/:filterId'); + + const routesMap = useMemo(() => keyBy(routesList, 'path'), []); + + const activeRoute = useMemo(() => { + if (editCalendarMatch) { + return routesMap[editCalendarMatch.path]; + } + if (editFilterMatch) { + return routesMap[editFilterMatch.path]; + } + // Remove trailing slash from the pathname + const pathnameKey = pathname.replace(/\/$/, ''); + return routesMap[pathnameKey]; + }, [pathname]); + + const bannerId = useRef(); + + useEffect( + function handleNotFoundRoute() { + if (!activeRoute && !!pathname) { + bannerId.current = overlays.banners.replace( + bannerId.current, + toMountPoint( + + } + data-test-subj={'mlPageNotFoundBanner'} + > +

+ +

+
, + { theme$: theme.theme$ } + ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + setTimeout(() => { + if (bannerId.current) { + overlays.banners.remove(bannerId.current); + } + }, 15000); + } + }, + [activeRoute, overlays, theme, pathname] + ); + + useExecutionContext(executionContext, { + name: 'Machine Learning', + type: 'application', + page: activeRoute?.path ?? '/overview', + }); + + return activeRoute ?? routesMap['/overview']; +}; diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 301fc5102a94f..e9ec6d7bfc1d6 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -9,9 +9,10 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { USER } from '../../../services/ml/security_common'; -export default function ({ getService }: FtrProviderContext) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); + const PageObjects = getPageObjects(['common', 'error']); const testUsers = [ { user: USER.ML_VIEWER, discoverAvailable: true }, @@ -100,6 +101,13 @@ export default function ({ getService }: FtrProviderContext) { await ml.overviewPage.assertDFACreateJobButtonExists(); await ml.overviewPage.assertDFACreateJobButtonEnabled(false); }); + + it('should redirect to the Overview page from the unrecognized routes', async () => { + await PageObjects.common.navigateToUrl('ml', 'magic-ai'); + + await ml.testExecution.logTestStep('should display a warning banner'); + await ml.overviewPage.assertPageNotFoundBannerText('magic-ai'); + }); }); } }); diff --git a/x-pack/test/functional/services/ml/overview_page.ts b/x-pack/test/functional/services/ml/overview_page.ts index 5f02edde0f310..9fd536b24e760 100644 --- a/x-pack/test/functional/services/ml/overview_page.ts +++ b/x-pack/test/functional/services/ml/overview_page.ts @@ -78,5 +78,17 @@ export function MachineLearningOverviewPageProvider({ getService }: FtrProviderC async assertJobSyncRequiredWarningNotExists() { await testSubjects.missingOrFail('mlJobSyncRequiredWarning', { timeout: 5000 }); }, + + async assertPageNotFoundBannerExists() { + await testSubjects.existOrFail('mlPageNotFoundBanner', { timeout: 5000 }); + }, + + async assertPageNotFoundBannerText(pathname: string) { + await this.assertPageNotFoundBannerExists(); + const text = await testSubjects.getVisibleText('mlPageNotFoundBannerText'); + expect(text).to.eql( + `The Machine Learning application doesn't recognize this route: /ml/${pathname}. You've been redirected to the Overview page.` + ); + }, }; }