mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add app error boundary
Should catch all app crashes
This commit is contained in:
parent
990b6b5f6a
commit
5c305b1eeb
@ -84,6 +84,7 @@
|
||||
"konva": "^9.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanostores": "^0.9.2",
|
||||
"new-github-issue-url": "^1.0.0",
|
||||
"openapi-fetch": "^0.6.1",
|
||||
"overlayscrollbars": "^2.2.0",
|
||||
"overlayscrollbars-react": "^0.5.0",
|
||||
@ -94,6 +95,7 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-hotkeys-hook": "4.4.0",
|
||||
"react-i18next": "^13.0.1",
|
||||
"react-icons": "^4.10.1",
|
||||
|
@ -16,9 +16,11 @@ import InvokeTabs from 'features/ui/components/InvokeTabs';
|
||||
import ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
||||
import i18n from 'i18n';
|
||||
import { size } from 'lodash-es';
|
||||
import { ReactNode, memo, useEffect } from 'react';
|
||||
import { ReactNode, memo, useCallback, useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import GlobalHotkeys from './GlobalHotkeys';
|
||||
import Toaster from './Toaster';
|
||||
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
||||
|
||||
const DEFAULT_CONFIG = {};
|
||||
|
||||
@ -32,6 +34,11 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
|
||||
const logger = useLogger();
|
||||
const dispatch = useAppDispatch();
|
||||
const handleReset = useCallback(() => {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language);
|
||||
@ -49,7 +56,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
onReset={handleReset}
|
||||
FallbackComponent={AppErrorBoundaryFallback}
|
||||
>
|
||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
<ImageUploader>
|
||||
<Grid
|
||||
@ -87,7 +97,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
||||
<ChangeBoardModal />
|
||||
<Toaster />
|
||||
<GlobalHotkeys />
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,97 @@
|
||||
import { Flex, Heading, Link, Text, useToast } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import newGithubIssueUrl from 'new-github-issue-url';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { FaCopy, FaExternalLinkAlt } from 'react-icons/fa';
|
||||
import { FaArrowRotateLeft } from 'react-icons/fa6';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
type Props = {
|
||||
error: Error;
|
||||
resetErrorBoundary: () => void;
|
||||
};
|
||||
|
||||
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
|
||||
const toast = useToast();
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const text = JSON.stringify(serializeError(error), null, 2);
|
||||
navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``);
|
||||
toast({
|
||||
title: 'Error Copied',
|
||||
});
|
||||
}, [error, toast]);
|
||||
|
||||
const url = useMemo(
|
||||
() =>
|
||||
newGithubIssueUrl({
|
||||
user: 'invoke-ai',
|
||||
repo: 'InvokeAI',
|
||||
template: 'BUG_REPORT.yml',
|
||||
title: `[bug]: ${error.name}: ${error.message}`,
|
||||
}),
|
||||
[error.message, error.name]
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="body"
|
||||
sx={{
|
||||
w: '100vw',
|
||||
h: '100vh',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
sx={{
|
||||
flexDir: 'column',
|
||||
borderRadius: 'base',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
p: 16,
|
||||
}}
|
||||
>
|
||||
<Heading>Something went wrong</Heading>
|
||||
<Flex
|
||||
layerStyle="second"
|
||||
sx={{
|
||||
px: 8,
|
||||
py: 4,
|
||||
borderRadius: 'base',
|
||||
gap: 4,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: 'error.500',
|
||||
_dark: { color: 'error.400' },
|
||||
}}
|
||||
>
|
||||
{error.name}: {error.message}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex sx={{ gap: 4 }}>
|
||||
<IAIButton
|
||||
leftIcon={<FaArrowRotateLeft />}
|
||||
onClick={resetErrorBoundary}
|
||||
>
|
||||
Reset UI
|
||||
</IAIButton>
|
||||
<IAIButton leftIcon={<FaCopy />} onClick={handleCopy}>
|
||||
Copy Error
|
||||
</IAIButton>
|
||||
<Link href={url} isExternal>
|
||||
<IAIButton leftIcon={<FaExternalLinkAlt />}>Create Issue</IAIButton>
|
||||
</Link>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppErrorBoundaryFallback;
|
@ -1,34 +1,35 @@
|
||||
import { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { isString } from 'lodash-es';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { FaCopy, FaSave } from 'react-icons/fa';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
jsonObject: object;
|
||||
data: object | string;
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
const ImageMetadataJSON = (props: Props) => {
|
||||
const { label, jsonObject, fileName } = props;
|
||||
const jsonString = useMemo(
|
||||
() => JSON.stringify(jsonObject, null, 2),
|
||||
[jsonObject]
|
||||
const DataViewer = (props: Props) => {
|
||||
const { label, data, fileName } = props;
|
||||
const dataString = useMemo(
|
||||
() => (isString(data) ? data : JSON.stringify(data, null, 2)),
|
||||
[data]
|
||||
);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(jsonString);
|
||||
}, [jsonString]);
|
||||
navigator.clipboard.writeText(dataString);
|
||||
}, [dataString]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const blob = new Blob([jsonString]);
|
||||
const blob = new Blob([dataString]);
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${fileName || label}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}, [jsonString, label, fileName]);
|
||||
}, [dataString, label, fileName]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -65,7 +66,7 @@ const ImageMetadataJSON = (props: Props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<pre>{jsonString}</pre>
|
||||
<pre>{dataString}</pre>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Box>
|
||||
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
|
||||
@ -92,4 +93,4 @@ const ImageMetadataJSON = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageMetadataJSON;
|
||||
export default DataViewer;
|
@ -16,7 +16,7 @@ import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||
import { ImageDTO } from 'services/api/types';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import ImageMetadataActions from './ImageMetadataActions';
|
||||
import ImageMetadataJSON from './ImageMetadataJSON';
|
||||
import DataViewer from './DataViewer';
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
image: ImageDTO;
|
||||
@ -79,21 +79,21 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
{metadata ? (
|
||||
<ImageMetadataJSON jsonObject={metadata} label="Core Metadata" />
|
||||
<DataViewer data={metadata} label="Core Metadata" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No core metadata found" />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{image ? (
|
||||
<ImageMetadataJSON jsonObject={image} label="Image Details" />
|
||||
<DataViewer data={image} label="Image Details" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No image details found" />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{graph ? (
|
||||
<ImageMetadataJSON jsonObject={graph} label="Graph" />
|
||||
<DataViewer data={graph} label="Graph" />
|
||||
) : (
|
||||
<IAINoContentFallback label="No graph found" />
|
||||
)}
|
||||
|
@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -30,7 +30,7 @@ const InspectorDataTab = () => {
|
||||
return <IAINoContentFallback label="No node selected" icon={null} />;
|
||||
}
|
||||
|
||||
return <ImageMetadataJSON jsonObject={data} label="Node Data" />;
|
||||
return <DataViewer data={data} label="Node Data" />;
|
||||
};
|
||||
|
||||
export default memo(InspectorDataTab);
|
||||
|
@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { memo } from 'react';
|
||||
|
||||
const selector = createSelector(
|
||||
@ -34,7 +34,7 @@ const NodeTemplateInspector = () => {
|
||||
return <IAINoContentFallback label="No node selected" icon={null} />;
|
||||
}
|
||||
|
||||
return <ImageMetadataJSON jsonObject={template} label="Node Template" />;
|
||||
return <DataViewer data={template} label="Node Template" />;
|
||||
};
|
||||
|
||||
export default memo(NodeTemplateInspector);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import ImageMetadataJSON from 'features/gallery/components/ImageMetadataViewer/ImageMetadataJSON';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { buildWorkflow } from 'features/nodes/util/buildWorkflow';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
@ -31,11 +31,7 @@ const WorkflowJSONTab = () => {
|
||||
h: 'full',
|
||||
}}
|
||||
>
|
||||
<ImageMetadataJSON
|
||||
jsonObject={workflow}
|
||||
label="Workflow"
|
||||
fileName={workflow.name}
|
||||
/>
|
||||
<DataViewer data={workflow} label="Workflow" fileName={workflow.name} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -4976,6 +4976,11 @@ neo-async@^2.6.0:
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
new-github-issue-url@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/new-github-issue-url/-/new-github-issue-url-1.0.0.tgz#c9e84057c2609b7cbd686d1d8baa53e291292e79"
|
||||
integrity sha512-wa9jlUFg3v6S3ddijQiB18SY4u9eJYcUe5sHa+6SB8m1UUbtX+H/bBglxOLnhhF1zIHuhWXnKBAa8kBeKRIozQ==
|
||||
|
||||
node-fetch@^2.6.11:
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
|
||||
@ -5488,6 +5493,13 @@ react-dropzone@^14.2.3:
|
||||
file-selector "^0.6.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-error-boundary@^4.0.11:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c"
|
||||
integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
react-fast-compare@3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f"
|
||||
|
Loading…
Reference in New Issue
Block a user