[APM] Add 404 page (#149471)

- 404 page
<img width="1644" alt="Screenshot 2023-01-24 at 6 46 44 PM"
src="https://user-images.githubusercontent.com/55978943/214448388-86d97ac9-f246-45f4-a5cb-58c6230faa83.png">

- Shows the error that happened while validating the params instead of a
blank page
<img width="1630" alt="Screenshot 2023-01-25 at 3 52 06 PM"
src="https://user-images.githubusercontent.com/55978943/214689993-e23dd588-9106-40dc-a447-4d20821b1bb2.png">


- Renders known routes
<img width="1650" alt="Screenshot 2023-01-24 at 6 46 57 PM"
src="https://user-images.githubusercontent.com/55978943/214448567-e2154db5-ff9d-47b8-834c-655b3d904631.png">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-02-02 13:52:51 -05:00 committed by GitHub
parent 82146cbd90
commit 74fc4f99d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 37 deletions

View file

@ -22,6 +22,12 @@ function toReactRouterPath(path: string) {
return path.replace(/(?:{([^\/]+)})/g, ':$1');
}
export class NotFoundRouteException extends Error {
constructor(message: string) {
super(message);
}
}
export function createRouter<TRoutes extends RouteMap>(routes: TRoutes): Router<TRoutes> {
const routesByReactRouterConfig = new Map<ReactRouterConfig, Route>();
const reactRouterConfigsByRoute = new Map<Route, ReactRouterConfig>();
@ -110,6 +116,11 @@ export function createRouter<TRoutes extends RouteMap>(routes: TRoutes): Router<
throw new Error(errorMessage);
}
const hasExactMatch = matches.some((match) => match.match.isExact);
if (!hasExactMatch) {
throw new NotFoundRouteException('No route was matched');
}
return matches.slice(0, matchIndex + 1).map((matchedRoute) => {
const route = routesByReactRouterConfig.get(matchedRoute.route);

View file

@ -7,6 +7,7 @@
*/
import { last } from 'lodash';
import { NotFoundRouteException } from './create_router';
import { useMatchRoutes } from './use_match_routes';
import { useRouter } from './use_router';
@ -14,7 +15,7 @@ export function useRoutePath() {
const lastRouteMatch = last(useMatchRoutes());
const router = useRouter();
if (!lastRouteMatch) {
throw new Error('No route was matched');
throw new NotFoundRouteException('No route was matched');
}
return router.getRoutePath(lastRouteMatch.route);

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
describe('404', () => {
beforeEach(() => {
cy.loginAsViewerUser();
});
it('Shows 404 page', () => {
cy.visitKibana('/app/apm/foo');
cy.contains('Page not found');
});
});

View file

@ -0,0 +1,66 @@
/*
* 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 { NotFoundRouteException } from '@kbn/typed-react-router-config';
import { EuiErrorBoundary } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found';
import { ApmPluginStartDeps } from '../../plugin';
export class ApmErrorBoundary extends React.Component<
{ children?: React.ReactNode },
{ error?: Error },
{}
> {
public state: { error?: Error } = {
error: undefined,
};
static getDerivedStateFromError(error: Error) {
return { error };
}
render() {
if (this.state.error) {
return <ErrorWithTemplate error={this.state.error} />;
}
return this.props.children;
}
}
const pageHeader = {
pageTitle: 'APM',
};
function ErrorWithTemplate({ error }: { error: Error }) {
const { services } = useKibana<ApmPluginStartDeps>();
const { observability } = services;
const ObservabilityPageTemplate = observability.navigation.PageTemplate;
if (error instanceof NotFoundRouteException) {
return (
<ObservabilityPageTemplate pageHeader={pageHeader}>
<NotFoundPrompt />
</ObservabilityPageTemplate>
);
}
return (
<ObservabilityPageTemplate pageHeader={pageHeader}>
<EuiErrorBoundary>
<DummyComponent error={error} />
</EuiErrorBoundary>
</ObservabilityPageTemplate>
);
}
function DummyComponent({ error }: { error: Error }) {
throw error;
return <div />;
}

View file

@ -5,23 +5,22 @@
* 2.0.
*/
import { euiLightVars, euiDarkVars } from '@kbn/ui-theme';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import React from 'react';
import { Route } from 'react-router-dom';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import {
KibanaContextProvider,
RedirectAppLinks,
useUiSetting$,
} from '@kbn/kibana-react-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
HeaderMenuPortal,
InspectorContextProvider,
} from '@kbn/observability-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { ScrollToTopOnPathChange } from '../app/main/scroll_to_top_on_path_change';
import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config';
import { euiDarkVars, euiLightVars } from '@kbn/ui-theme';
import React from 'react';
import { Route } from 'react-router-dom';
import { DefaultTheme, ThemeProvider } from 'styled-components';
import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
import {
ApmPluginContext,
@ -33,13 +32,15 @@ import { LicenseProvider } from '../../context/license/license_context';
import { TimeRangeIdContextProvider } from '../../context/time_range_id/time_range_id_context';
import { UrlParamsProvider } from '../../context/url_params_context/url_params_context';
import { ApmPluginStartDeps } from '../../plugin';
import { ScrollToTopOnPathChange } from '../app/main/scroll_to_top_on_path_change';
import { ApmHeaderActionMenu } from '../shared/apm_header_action_menu';
import { RedirectWithDefaultDateRange } from '../shared/redirect_with_default_date_range';
import { apmRouter } from './apm_route_config';
import { TrackPageview } from './track_pageview';
import { RedirectWithDefaultEnvironment } from '../shared/redirect_with_default_environment';
import { RedirectWithOffset } from '../shared/redirect_with_offset';
import { ApmErrorBoundary } from './apm_error_boundary';
import { apmRouter } from './apm_route_config';
import { RedirectDependenciesToDependenciesInventory } from './home/redirect_dependencies_to_dependencies_inventory';
import { TrackPageview } from './track_pageview';
const storage = new Storage(localStorage);
@ -66,34 +67,36 @@ export function ApmAppRoot({
<i18nCore.Context>
<TimeRangeIdContextProvider>
<RouterProvider history={history} router={apmRouter as any}>
<RedirectDependenciesToDependenciesInventory>
<RedirectWithDefaultEnvironment>
<RedirectWithDefaultDateRange>
<RedirectWithOffset>
<TrackPageview>
<BreadcrumbsContextProvider>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<InspectorContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />
<ApmErrorBoundary>
<RedirectDependenciesToDependenciesInventory>
<RedirectWithDefaultEnvironment>
<RedirectWithDefaultDateRange>
<RedirectWithOffset>
<TrackPageview>
<BreadcrumbsContextProvider>
<UrlParamsProvider>
<LicenseProvider>
<AnomalyDetectionJobsContextProvider>
<InspectorContextProvider>
<ApmThemeProvider>
<MountApmHeaderActionMenu />
<Route
component={ScrollToTopOnPathChange}
/>
<RouteRenderer />
</ApmThemeProvider>
</InspectorContextProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</BreadcrumbsContextProvider>
</TrackPageview>
</RedirectWithOffset>
</RedirectWithDefaultDateRange>
</RedirectWithDefaultEnvironment>
</RedirectDependenciesToDependenciesInventory>
<Route
component={ScrollToTopOnPathChange}
/>
<RouteRenderer />
</ApmThemeProvider>
</InspectorContextProvider>
</AnomalyDetectionJobsContextProvider>
</LicenseProvider>
</UrlParamsProvider>
</BreadcrumbsContextProvider>
</TrackPageview>
</RedirectWithOffset>
</RedirectWithDefaultDateRange>
</RedirectWithDefaultEnvironment>
</RedirectDependenciesToDependenciesInventory>
</ApmErrorBoundary>
</RouterProvider>
</TimeRangeIdContextProvider>
</i18nCore.Context>

View file

@ -76,6 +76,7 @@
"@kbn/babel-register",
"@kbn/core-saved-objects-migration-server-internal",
"@kbn/core-elasticsearch-server",
"@kbn/shared-ux-prompt-not-found",
"@kbn/core-saved-objects-api-server",
],
"exclude": [