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

View File

@@ -3,6 +3,8 @@ import { QueryResult } from "@apollo/client";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { Pagination, PaginationIndex } from "./Pagination"; import { Pagination, PaginationIndex } from "./Pagination";
import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { ErrorMessage } from "../Shared/ErrorMessage";
import { FormattedMessage } from "react-intl";
export const PagedList: React.FC< export const PagedList: React.FC<
PropsWithChildren<{ PropsWithChildren<{
@@ -65,7 +67,17 @@ export const PagedList: React.FC<
return <LoadingIndicator />; return <LoadingIndicator />;
} }
if (result.error) { 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 ( return (

View File

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

View File

@@ -213,13 +213,29 @@ button.collapse-button {
text-align: center; text-align: center;
} }
.ErrorMessage { .ErrorMessage-container {
align-items: center; display: flex;
height: 20rem;
justify-content: center; justify-content: center;
width: 100%;
}
&-content { .ErrorMessage {
display: inline-block; .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; 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 }) => { export const ToastProvider: React.FC = ({ children }) => {
const [toast, setToast] = useState<IActiveToast>(); const [toast, setToast] = useState<IActiveToast>();
@@ -121,6 +123,10 @@ export const ToastProvider: React.FC = ({ children }) => {
export const useToast = () => { export const useToast = () => {
const addToast = useContext(ToastContext); const addToast = useContext(ToastContext);
if (!addToast) {
throw new Error("useToast must be used within a ToastProvider");
}
return useMemo( return useMemo(
() => ({ () => ({
toast: addToast, toast: addToast,

View File

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