diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index 1ea6355b9c558..baa7ffc5b8de3 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -41,6 +41,7 @@ import { PluginInitializerContext, ScopedHistory, } from '../services/core'; +import { DashboardNoMatch } from './listing/dashboard_no_match'; export const dashboardUrlParams = { showTopMenu: 'show-top-menu', @@ -77,6 +78,7 @@ export async function mountApp({ const { navigation, savedObjects, + urlForwarding, data: dataStart, share: shareStart, embeddable: embeddableStart, @@ -88,6 +90,7 @@ export async function mountApp({ navigation, onAppLeave, savedObjects, + urlForwarding, usageCollection, core: coreStart, data: dataStart, @@ -180,6 +183,10 @@ export async function mountApp({ ); }; + const renderNoMatch = (routeProps: RouteComponentProps) => { + return ; + }; + // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); @@ -202,9 +209,10 @@ export async function mountApp({ render={renderDashboard} /> - + + diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index 3aee05554b0d9..8172be46e9f3a 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -39,6 +39,7 @@ import { dataPluginMock } from '../../../../data/public/mocks'; import { chromeServiceMock, coreMock } from '../../../../../core/public/mocks'; import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; +import { UrlForwardingStart } from '../../../../url_forwarding/public'; function makeDefaultServices(): DashboardAppServices { const core = coreMock.createStart(); @@ -71,6 +72,7 @@ function makeDefaultServices(): DashboardAppServices { scopedHistory: () => ({} as ScopedHistory), savedQueryService: {} as SavedQueryService, setHeaderActionMenu: (mountPoint) => {}, + urlForwarding: {} as UrlForwardingStart, uiSettings: {} as IUiSettingsClient, restorePreviousUrl: () => {}, onAppLeave: (handler) => {}, diff --git a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx new file mode 100644 index 0000000000000..a0f13af92ff77 --- /dev/null +++ b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut } from '@elastic/eui'; + +import { RouteComponentProps } from 'react-router-dom'; +import { useKibana, toMountPoint } from '../../services/kibana_react'; +import { DashboardAppServices } from '../types'; +import { DashboardConstants } from '../..'; + +let bannerId: string | undefined; + +export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['history'] }) => { + const { services } = useKibana(); + + useEffect(() => { + services.restorePreviousUrl(); + + const { navigated } = services.urlForwarding.navigateToLegacyKibanaUrl( + history.location.pathname + ); + + if (!navigated) { + const bannerMessage = i18n.translate('dashboard.noMatchRoute.bannerTitleText', { + defaultMessage: 'Page not found', + }); + + bannerId = services.core.overlays.banners.replace( + bannerId, + toMountPoint( + +

+ +

+
+ ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + setTimeout(() => { + if (bannerId) { + services.core.overlays.banners.remove(bannerId); + } + }, 15000); + + history.replace(DashboardConstants.LANDING_PAGE_PATH); + } + }, [services, history]); + + return null; +}; diff --git a/src/plugins/dashboard/public/application/types.ts b/src/plugins/dashboard/public/application/types.ts index d1caaa349d80b..75620fd73360d 100644 --- a/src/plugins/dashboard/public/application/types.ts +++ b/src/plugins/dashboard/public/application/types.ts @@ -33,6 +33,7 @@ import { NavigationPublicPluginStart } from '../services/navigation'; import { SavedObjectsTaggingApi } from '../services/saved_objects_tagging_oss'; import { DataPublicPluginStart, IndexPatternsContract } from '../services/data'; import { SavedObjectLoader, SavedObjectsStart } from '../services/saved_objects'; +import { UrlForwardingStart } from '../../../url_forwarding/public'; export type DashboardRedirect = (props: RedirectToProps) => void; export type RedirectToProps = @@ -75,6 +76,7 @@ export interface DashboardAppServices { uiSettings: IUiSettingsClient; restorePreviousUrl: () => void; savedObjects: SavedObjectsStart; + urlForwarding: UrlForwardingStart; savedDashboards: SavedObjectLoader; scopedHistory: () => ScopedHistory; indexPatterns: IndexPatternsContract; diff --git a/test/functional/apps/dashboard/legacy_urls.ts b/test/functional/apps/dashboard/legacy_urls.ts index 6bb8d808e8daa..2a30bbf5e1f0a 100644 --- a/test/functional/apps/dashboard/legacy_urls.ts +++ b/test/functional/apps/dashboard/legacy_urls.ts @@ -92,6 +92,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visEditor.setMarkdownTxt(`[abc](#/dashboard/${testDashboardId})`); await PageObjects.visEditor.clickGo(); + + await PageObjects.visualize.saveVisualizationExpectSuccess('legacy url markdown'); + (await find.byLinkText('abc')).click(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -109,6 +112,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.expectMarkdownTextArea(); await browser.goForward(); }); + + it('resolves markdown link from dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.addVisualization('legacy url markdown'); + (await find.byLinkText('abc')).click(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.timePicker.setDefaultDataRange(); + + await PageObjects.dashboard.waitForRenderComplete(); + await pieChart.expectPieSliceCount(5); + }); }); }); }