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",
|
"konva": "^9.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanostores": "^0.9.2",
|
"nanostores": "^0.9.2",
|
||||||
|
"new-github-issue-url": "^1.0.0",
|
||||||
"openapi-fetch": "^0.6.1",
|
"openapi-fetch": "^0.6.1",
|
||||||
"overlayscrollbars": "^2.2.0",
|
"overlayscrollbars": "^2.2.0",
|
||||||
"overlayscrollbars-react": "^0.5.0",
|
"overlayscrollbars-react": "^0.5.0",
|
||||||
@ -94,6 +95,7 @@
|
|||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
|
"react-error-boundary": "^4.0.11",
|
||||||
"react-hotkeys-hook": "4.4.0",
|
"react-hotkeys-hook": "4.4.0",
|
||||||
"react-i18next": "^13.0.1",
|
"react-i18next": "^13.0.1",
|
||||||
"react-icons": "^4.10.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 ParametersDrawer from 'features/ui/components/ParametersDrawer';
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { size } from 'lodash-es';
|
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 GlobalHotkeys from './GlobalHotkeys';
|
||||||
import Toaster from './Toaster';
|
import Toaster from './Toaster';
|
||||||
|
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {};
|
const DEFAULT_CONFIG = {};
|
||||||
|
|
||||||
@ -32,6 +34,11 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
|
|
||||||
const logger = useLogger();
|
const logger = useLogger();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const handleReset = useCallback(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
location.reload();
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
i18n.changeLanguage(language);
|
i18n.changeLanguage(language);
|
||||||
@ -49,7 +56,10 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ErrorBoundary
|
||||||
|
onReset={handleReset}
|
||||||
|
FallbackComponent={AppErrorBoundaryFallback}
|
||||||
|
>
|
||||||
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||||
<ImageUploader>
|
<ImageUploader>
|
||||||
<Grid
|
<Grid
|
||||||
@ -87,7 +97,7 @@ const App = ({ config = DEFAULT_CONFIG, headerComponent }: Props) => {
|
|||||||
<ChangeBoardModal />
|
<ChangeBoardModal />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<GlobalHotkeys />
|
<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 { Box, Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { isString } from 'lodash-es';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { FaCopy, FaSave } from 'react-icons/fa';
|
import { FaCopy, FaSave } from 'react-icons/fa';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string;
|
label: string;
|
||||||
jsonObject: object;
|
data: object | string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageMetadataJSON = (props: Props) => {
|
const DataViewer = (props: Props) => {
|
||||||
const { label, jsonObject, fileName } = props;
|
const { label, data, fileName } = props;
|
||||||
const jsonString = useMemo(
|
const dataString = useMemo(
|
||||||
() => JSON.stringify(jsonObject, null, 2),
|
() => (isString(data) ? data : JSON.stringify(data, null, 2)),
|
||||||
[jsonObject]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
navigator.clipboard.writeText(jsonString);
|
navigator.clipboard.writeText(dataString);
|
||||||
}, [jsonString]);
|
}, [dataString]);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
const blob = new Blob([jsonString]);
|
const blob = new Blob([dataString]);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
a.download = `${fileName || label}.json`;
|
a.download = `${fileName || label}.json`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
}, [jsonString, label, fileName]);
|
}, [dataString, label, fileName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -65,7 +66,7 @@ const ImageMetadataJSON = (props: Props) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre>{jsonString}</pre>
|
<pre>{dataString}</pre>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</Box>
|
</Box>
|
||||||
<Flex sx={{ position: 'absolute', top: 0, insetInlineEnd: 0, p: 2 }}>
|
<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 { ImageDTO } from 'services/api/types';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
import ImageMetadataActions from './ImageMetadataActions';
|
import ImageMetadataActions from './ImageMetadataActions';
|
||||||
import ImageMetadataJSON from './ImageMetadataJSON';
|
import DataViewer from './DataViewer';
|
||||||
|
|
||||||
type ImageMetadataViewerProps = {
|
type ImageMetadataViewerProps = {
|
||||||
image: ImageDTO;
|
image: ImageDTO;
|
||||||
@ -79,21 +79,21 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
|
|||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{metadata ? (
|
{metadata ? (
|
||||||
<ImageMetadataJSON jsonObject={metadata} label="Core Metadata" />
|
<DataViewer data={metadata} label="Core Metadata" />
|
||||||
) : (
|
) : (
|
||||||
<IAINoContentFallback label="No core metadata found" />
|
<IAINoContentFallback label="No core metadata found" />
|
||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{image ? (
|
{image ? (
|
||||||
<ImageMetadataJSON jsonObject={image} label="Image Details" />
|
<DataViewer data={image} label="Image Details" />
|
||||||
) : (
|
) : (
|
||||||
<IAINoContentFallback label="No image details found" />
|
<IAINoContentFallback label="No image details found" />
|
||||||
)}
|
)}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{graph ? (
|
{graph ? (
|
||||||
<ImageMetadataJSON jsonObject={graph} label="Graph" />
|
<DataViewer data={graph} label="Graph" />
|
||||||
) : (
|
) : (
|
||||||
<IAINoContentFallback label="No graph found" />
|
<IAINoContentFallback label="No graph found" />
|
||||||
)}
|
)}
|
||||||
|
@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
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';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
@ -30,7 +30,7 @@ const InspectorDataTab = () => {
|
|||||||
return <IAINoContentFallback label="No node selected" icon={null} />;
|
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);
|
export default memo(InspectorDataTab);
|
||||||
|
@ -3,7 +3,7 @@ import { stateSelector } from 'app/store/store';
|
|||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
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';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
@ -34,7 +34,7 @@ const NodeTemplateInspector = () => {
|
|||||||
return <IAINoContentFallback label="No node selected" icon={null} />;
|
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);
|
export default memo(NodeTemplateInspector);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
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 { buildWorkflow } from 'features/nodes/util/buildWorkflow';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
@ -31,11 +31,7 @@ const WorkflowJSONTab = () => {
|
|||||||
h: 'full',
|
h: 'full',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ImageMetadataJSON
|
<DataViewer data={workflow} label="Workflow" fileName={workflow.name} />
|
||||||
jsonObject={workflow}
|
|
||||||
label="Workflow"
|
|
||||||
fileName={workflow.name}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4976,6 +4976,11 @@ neo-async@^2.6.0:
|
|||||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
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:
|
node-fetch@^2.6.11:
|
||||||
version "2.6.12"
|
version "2.6.12"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.12.tgz#02eb8e22074018e3d5a83016649d04df0e348fba"
|
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"
|
file-selector "^0.6.0"
|
||||||
prop-types "^15.8.1"
|
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:
|
react-fast-compare@3.2.1:
|
||||||
version "3.2.1"
|
version "3.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f"
|
||||||
|
Loading…
Reference in New Issue
Block a user