Error loading plugins (#5813)

* Improve error messages when unable to contact server
* Improve error message presentation
* Catch errors when configuration can't be loaded
* Use ErrorMessage in PagedList
* Add icon to error message
This commit is contained in:
WithoutPants
2025-06-11 16:54:11 +10:00
committed by GitHub
parent ed4d17b8f0
commit 3d03072da0
6 changed files with 132 additions and 45 deletions

View File

@@ -6,7 +6,7 @@ import {
useLocation,
useRouteMatch,
} from "react-router-dom";
import { IntlProvider, CustomFormats } from "react-intl";
import { IntlProvider, CustomFormats, FormattedMessage } from "react-intl";
import { Helmet } from "react-helmet";
import cloneDeep from "lodash-es/cloneDeep";
import mergeWith from "lodash-es/mergeWith";
@@ -49,6 +49,7 @@ import { ConnectionMonitor } from "./ConnectionMonitor";
import { PatchFunction } from "./patch";
import moment from "moment/min/moment-with-locales";
import { ErrorMessage } from "./components/Shared/ErrorMessage";
const Performers = lazyComponent(
() => import("./components/Performers/Performers")
@@ -102,6 +103,14 @@ const AppContainer: React.FC<React.PropsWithChildren<{}>> = PatchFunction(
}
) as React.FC;
const MainContainer: React.FC = ({ children }) => {
return (
<div className={`main container-fluid ${appleRendering ? "apple" : ""}`}>
{children}
</div>
);
};
function translateLanguageLocale(l: string) {
// intl doesn't support all locales, so we need to map some to supported ones
switch (l) {
@@ -287,14 +296,40 @@ export const App: React.FC = () => {
const titleProps = makeTitleProps();
if (!messages) {
return null;
}
if (config.error) {
return (
<IntlProvider
locale={intlLanguage}
messages={messages}
formats={intlFormats}
>
<MainContainer>
<ErrorMessage
message={
<FormattedMessage
id="errors.loading_type"
values={{ type: "configuration" }}
/>
}
error={config.error.message}
/>
</MainContainer>
</IntlProvider>
);
}
return (
<ErrorBoundary>
{messages ? (
<IntlProvider
locale={intlLanguage}
messages={messages}
formats={intlFormats}
>
<IntlProvider
locale={intlLanguage}
messages={messages}
formats={intlFormats}
>
<ToastProvider>
<PluginsLoader>
<AppContainer>
<ConfigurationProvider
@@ -302,31 +337,23 @@ export const App: React.FC = () => {
loading={config.loading}
>
{maybeRenderReleaseNotes()}
<ToastProvider>
<ConnectionMonitor />
<Suspense fallback={<LoadingIndicator />}>
<LightboxProvider>
<ManualProvider>
<InteractiveProvider>
<Helmet {...titleProps} />
{maybeRenderNavbar()}
<div
className={`main container-fluid ${
appleRendering ? "apple" : ""
}`}
>
{renderContent()}
</div>
</InteractiveProvider>
</ManualProvider>
</LightboxProvider>
</Suspense>
</ToastProvider>
<ConnectionMonitor />
<Suspense fallback={<LoadingIndicator />}>
<LightboxProvider>
<ManualProvider>
<InteractiveProvider>
<Helmet {...titleProps} />
{maybeRenderNavbar()}
<MainContainer>{renderContent()}</MainContainer>
</InteractiveProvider>
</ManualProvider>
</LightboxProvider>
</Suspense>
</ConfigurationProvider>
</AppContainer>
</PluginsLoader>
</IntlProvider>
) : null}
</ToastProvider>
</IntlProvider>
</ErrorBoundary>
);
};

View File

@@ -3,6 +3,8 @@ import { QueryResult } from "@apollo/client";
import { ListFilterModel } from "src/models/list-filter/filter";
import { Pagination, PaginationIndex } from "./Pagination";
import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { ErrorMessage } from "../Shared/ErrorMessage";
import { FormattedMessage } from "react-intl";
export const PagedList: React.FC<
PropsWithChildren<{
@@ -65,7 +67,17 @@ export const PagedList: React.FC<
return <LoadingIndicator />;
}
if (result.error) {
return <h1>{result.error.message}</h1>;
return (
<ErrorMessage
message={
<FormattedMessage
id="errors.loading_type"
values={{ type: "items" }}
/>
}
error={result.error.message}
/>
);
}
return (

View File

@@ -1,11 +1,26 @@
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import React, { ReactNode } from "react";
import { Alert } from "react-bootstrap";
import { FormattedMessage } from "react-intl";
import { Icon } from "./Icon";
interface IProps {
message?: React.ReactNode;
error: string | ReactNode;
}
export const ErrorMessage: React.FC<IProps> = ({ error }) => (
<div className="row ErrorMessage">
<h2 className="ErrorMessage-content">Error: {error}</h2>
</div>
);
export const ErrorMessage: React.FC<IProps> = (props) => {
const { error, message = <FormattedMessage id="errors.header" /> } = props;
return (
<div className="ErrorMessage-container">
<Alert variant="danger" className="ErrorMessage">
<Alert.Heading className="ErrorMessage-header">
<Icon icon={faWarning} />
{message}
</Alert.Heading>
<div className="ErrorMessage-content code">{error}</div>
</Alert>
</div>
);
};

View File

@@ -213,13 +213,29 @@ button.collapse-button {
text-align: center;
}
.ErrorMessage {
align-items: center;
height: 20rem;
.ErrorMessage-container {
display: flex;
justify-content: center;
width: 100%;
}
&-content {
display: inline-block;
.ErrorMessage {
.fa-icon {
color: $warning;
font-size: 1.5em;
margin-right: 0.3em;
vertical-align: middle;
}
background-color: initial;
border-color: $danger;
color: $text-color;
margin: 1rem;
text-align: left;
width: 500px;
@include media-breakpoint-down(xs) {
width: 100%;
}
}

View File

@@ -28,7 +28,9 @@ const errorDelay = 5000;
let toastID = 0;
const ToastContext = createContext<(item: IToast) => void>(() => {});
type ToastFn = (item: IToast) => void;
const ToastContext = createContext<ToastFn | null>(null);
export const ToastProvider: React.FC = ({ children }) => {
const [toast, setToast] = useState<IActiveToast>();
@@ -121,6 +123,10 @@ export const ToastProvider: React.FC = ({ children }) => {
export const useToast = () => {
const addToast = useContext(ToastContext);
if (!addToast) {
throw new Error("useToast must be used within a ToastProvider");
}
return useMemo(
() => ({
toast: addToast,

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect } from "react";
import { PatchFunction } from "./patch";
import { usePlugins } from "./core/StashService";
import { useMemoOnce } from "./hooks/state";
@@ -7,6 +7,7 @@ import useScript, { useCSS } from "./hooks/useScript";
import { PluginsQuery } from "./core/generated-graphql";
import { LoadingIndicator } from "./components/Shared/LoadingIndicator";
import { FormattedMessage } from "react-intl";
import { useToast } from "./hooks/Toast";
type PluginList = NonNullable<Required<PluginsQuery["plugins"]>>;
@@ -102,15 +103,25 @@ function useLoadPlugins() {
);
useCSS(pluginCSS ?? [], !pluginsLoading && !pluginsError);
return !pluginsLoading && !!pluginJavascripts && pluginJavascriptLoaded;
return {
loading: !pluginsLoading && !!pluginJavascripts && pluginJavascriptLoaded,
error: pluginsError,
};
}
export const PluginsLoader: React.FC<React.PropsWithChildren<{}>> = ({
children,
}) => {
const loaded = useLoadPlugins();
const Toast = useToast();
const { loading: loaded, error } = useLoadPlugins();
if (!loaded)
useEffect(() => {
if (error) {
Toast.error(`Error loading plugins: ${error.message}`);
}
}, [Toast, error]);
if (!loaded && !error)
return (
<LoadingIndicator message={<FormattedMessage id="loading.plugins" />} />
);