feat(ui): add app error boundary

Should catch all app crashes
This commit is contained in:
psychedelicious 2023-08-21 00:32:11 +10:00
parent 990b6b5f6a
commit 5c305b1eeb
9 changed files with 147 additions and 29 deletions

View File

@ -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",

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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" />
)}

View File

@ -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);

View File

@ -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);

View File

@ -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>
);
};

View File

@ -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"