frontend-defaults: new display for app startup errors

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-09-08 10:21:58 +02:00
parent 02fced51db
commit fa4cd1d47f
4 changed files with 135 additions and 0 deletions
+12
View File
@@ -3,6 +3,8 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AppError } from '@backstage/frontend-app-api';
import { AppErrorTypes } from '@backstage/frontend-app-api';
import { Config } from '@backstage/config';
import { ConfigApi } from '@backstage/frontend-plugin-api';
import { CreateAppRouteBinder } from '@backstage/frontend-app-api';
@@ -45,6 +47,16 @@ export function discoverAvailableFeatures(config: Config): {
features: (FrontendFeature | FrontendFeatureLoader)[];
};
// @public
export function maybeCreateErrorPage(
app: {
errors?: AppError[];
},
options?: {
warningCodes?: Array<keyof AppErrorTypes>;
},
): JSX_2.Element | undefined;
// @public (undocumented)
export function resolveAsyncFeatures(options: {
config: Config;
@@ -36,6 +36,7 @@ import {
import appPlugin from '@backstage/plugin-app';
import { discoverAvailableFeatures } from './discovery';
import { resolveAsyncFeatures } from './resolution';
import { maybeCreateErrorPage } from './maybeCreateErrorPage';
/**
* Options for {@link createApp}.
@@ -136,6 +137,11 @@ export function createApp(options?: CreateAppOptions): {
advanced: options?.advanced,
});
const errorPage = maybeCreateErrorPage(app);
if (errorPage) {
return { default: () => errorPage };
}
const rootEl = app.tree.root.instance!.getData(
coreExtensionData.reactElement,
);
+1
View File
@@ -24,3 +24,4 @@ export { createApp, type CreateAppOptions } from './createApp';
export { createPublicSignInApp } from './createPublicSignInApp';
export { discoverAvailableFeatures } from './discovery';
export { resolveAsyncFeatures } from './resolution';
export { maybeCreateErrorPage } from './maybeCreateErrorPage';
@@ -0,0 +1,116 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed 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 { FrontendPluginInfo } from '@backstage/frontend-plugin-api';
import { JSX, useEffect, useState } from 'react';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppError, AppErrorTypes } from '@backstage/frontend-app-api';
const DEFAULT_WARNING_CODES: Array<keyof AppErrorTypes> = [
'EXTENSION_IGNORED',
'INVALID_EXTENSION_CONFIG_KEY',
'EXTENSION_INPUT_DATA_IGNORED',
'EXTENSION_OUTPUT_IGNORED',
];
function AppErrorItem(props: { error: AppError }): JSX.Element {
const { context } = props.error;
const extensionId =
'extensionId' in context ? context.extensionId : context.node.spec.id;
const node = 'node' in context ? context.node : undefined;
const plugin = 'plugin' in context ? context.plugin : node?.spec.plugin;
const pluginId = plugin?.id ?? 'N/A';
const [info, setInfo] = useState<FrontendPluginInfo | undefined>(undefined);
useEffect(() => {
plugin?.info().then(setInfo, error => {
// eslint-disable-next-line no-console
console.error(`Failed to load info for plugin ${plugin.id}: ${error}`);
});
}, [plugin]);
return (
<div>
<b>{props.error.code}</b>: {props.error.message}
<pre style={{ marginLeft: '1rem' }}>
<div>extensionId: {extensionId}</div>
{pluginId && <div>pluginId: {pluginId}</div>}
{info && (
<div>
package: {info.packageName}@{info.version}
</div>
)}
</pre>
</div>
);
}
function AppErrorPage(props: { errors: AppError[] }): JSX.Element {
return (
<div style={{ margin: '1rem' }}>
<h2>App startup failed</h2>
{props.errors.map((error, index) => (
<AppErrorItem error={error} key={index} />
))}
</div>
);
}
/**
* If there are any unrecoverable errors in the app, this will return an error page in the form of a JSX element.
*
* If there are any recoverable errors, they will always be logged as warnings in the console.
* @public
*/
export function maybeCreateErrorPage(
app: { errors?: AppError[] },
options?: {
warningCodes?: Array<keyof AppErrorTypes>;
},
): JSX.Element | undefined {
if (!app.errors) {
return undefined;
}
const errors = new Array<AppError>();
const warnings = new Array<AppError>();
const warningCodes = new Set(options?.warningCodes ?? DEFAULT_WARNING_CODES);
for (const error of app.errors) {
if (warningCodes.has(error.code)) {
warnings.push(error);
} else {
errors.push(error);
}
}
if (warnings.length > 0) {
// eslint-disable-next-line no-console
console.warn('App startup encountered warnings:');
for (const warning of warnings) {
// eslint-disable-next-line no-console
console.warn(`${warning.code}: ${warning.message}`);
}
}
if (errors.length === 0) {
return undefined;
}
return <AppErrorPage errors={errors} />;
}