mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
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:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" />} />
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user