mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
perf(ui): optimize workflow editor inspector panel rendering
This commit is contained in:
@@ -5,18 +5,20 @@ import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataView
|
||||
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
const selector = createSelector(selectNodesSlice, (nodes) => selectLastSelectedNode(nodes)?.data);
|
||||
|
||||
const InspectorDataTab = () => {
|
||||
const { t } = useTranslation();
|
||||
const lastSelectedNodeData = useAppSelector(selector);
|
||||
const [debouncedLastSelectedNodeData] = useDebounce(lastSelectedNodeData, 300);
|
||||
|
||||
if (!lastSelectedNodeData) {
|
||||
if (!debouncedLastSelectedNodeData) {
|
||||
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
||||
}
|
||||
|
||||
return <DataViewer data={lastSelectedNodeData} label="Node Data" />;
|
||||
return <DataViewer data={debouncedLastSelectedNodeData} label="Node Data" />;
|
||||
};
|
||||
|
||||
export default memo(InspectorDataTab);
|
||||
|
||||
@@ -1,82 +1,64 @@
|
||||
import { Box, Flex, FormControl, FormLabel, HStack, Text } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { InvocationNodeNotesTextarea } from 'features/nodes/components/flow/nodes/Invocation/InvocationNodeNotesTextarea';
|
||||
import { useNodeIsInvocationNode } from 'features/nodes/hooks/useNodeIsInvocationNode';
|
||||
import { TemplateGate } from 'features/nodes/components/sidePanel/inspector/NodeTemplateGate';
|
||||
import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||
import { useNodeVersion } from 'features/nodes/hooks/useNodeVersion';
|
||||
import { selectLastSelectedNodeId } from 'features/nodes/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import EditableNodeTitle from './details/EditableNodeTitle';
|
||||
|
||||
const InspectorDetailsTab = () => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||
const lastSelectedNode = selectLastSelectedNode(nodes);
|
||||
const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined;
|
||||
|
||||
if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
nodeId: lastSelectedNode.data.id,
|
||||
nodeVersion: lastSelectedNode.data.version,
|
||||
templateTitle: lastSelectedNodeTemplate.title,
|
||||
};
|
||||
}),
|
||||
[templates]
|
||||
);
|
||||
const data = useAppSelector(selector);
|
||||
const { t } = useTranslation();
|
||||
const lastSelectedNodeId = useAppSelector(selectLastSelectedNodeId);
|
||||
|
||||
if (!data) {
|
||||
if (!lastSelectedNodeId) {
|
||||
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
||||
}
|
||||
|
||||
return <Content nodeId={data.nodeId} nodeVersion={data.nodeVersion} templateTitle={data.templateTitle} />;
|
||||
return (
|
||||
<TemplateGate
|
||||
nodeId={lastSelectedNodeId}
|
||||
fallback={<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />}
|
||||
>
|
||||
<Content nodeId={lastSelectedNodeId} />
|
||||
</TemplateGate>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InspectorDetailsTab);
|
||||
|
||||
type ContentProps = {
|
||||
nodeId: string;
|
||||
nodeVersion: string;
|
||||
templateTitle: string;
|
||||
};
|
||||
|
||||
const Content = memo((props: ContentProps) => {
|
||||
const Content = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const needsUpdate = useNodeNeedsUpdate(props.nodeId);
|
||||
const isInvocationNode = useNodeIsInvocationNode(props.nodeId);
|
||||
const version = useNodeVersion(nodeId);
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const needsUpdate = useNodeNeedsUpdate(nodeId);
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column" position="relative" w="full" h="full" p={1} gap={2}>
|
||||
<EditableNodeTitle nodeId={props.nodeId} />
|
||||
<EditableNodeTitle nodeId={nodeId} />
|
||||
<HStack>
|
||||
<FormControl>
|
||||
<FormLabel>{t('nodes.nodeType')}</FormLabel>
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
{props.templateTitle}
|
||||
{template.title}
|
||||
</Text>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={needsUpdate}>
|
||||
<FormLabel>{t('nodes.nodeVersion')}</FormLabel>
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
{props.nodeVersion}
|
||||
{version}
|
||||
</Text>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
{isInvocationNode && <InvocationNodeNotesTextarea nodeId={props.nodeId} />}
|
||||
<InvocationNodeNotesTextarea nodeId={nodeId} />
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { TemplateGate } from 'features/nodes/components/sidePanel/inspector/NodeTemplateGate';
|
||||
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||
import { selectLastSelectedNodeId } from 'features/nodes/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AnyInvocationOutput, ImageOutput } from 'services/api/types';
|
||||
|
||||
import ImageOutputPreview from './outputs/ImageOutputPreview';
|
||||
|
||||
const InspectorOutputsTab = () => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||
const lastSelectedNode = selectLastSelectedNode(nodes);
|
||||
const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined;
|
||||
|
||||
if (!isInvocationNode(lastSelectedNode) || !lastSelectedNodeTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
nodeId: lastSelectedNode.id,
|
||||
outputType: lastSelectedNodeTemplate.outputType,
|
||||
};
|
||||
}),
|
||||
[templates]
|
||||
);
|
||||
const data = useAppSelector(selector);
|
||||
const nes = useNodeExecutionState(data?.nodeId);
|
||||
const lastSelectedNodeId = useAppSelector(selectLastSelectedNodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!data || !nes) {
|
||||
if (!lastSelectedNodeId) {
|
||||
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
||||
}
|
||||
|
||||
if (nes.outputs.length === 0) {
|
||||
return (
|
||||
<TemplateGate
|
||||
nodeId={lastSelectedNodeId}
|
||||
fallback={<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />}
|
||||
>
|
||||
<Content nodeId={lastSelectedNodeId} />
|
||||
</TemplateGate>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InspectorOutputsTab);
|
||||
|
||||
const getKey = (result: AnyInvocationOutput, i: number) => `${result.type}-${i}`;
|
||||
|
||||
const Content = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const template = useNodeTemplate(nodeId);
|
||||
const nes = useNodeExecutionState(nodeId);
|
||||
|
||||
if (!nes || nes.outputs.length === 0) {
|
||||
return <IAINoContentFallback label={t('nodes.noOutputRecorded')} icon={null} />;
|
||||
}
|
||||
|
||||
@@ -50,7 +48,7 @@ const InspectorOutputsTab = () => {
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<Flex position="relative" flexDir="column" alignItems="flex-start" p={1} gap={2} h="full" w="full">
|
||||
{data.outputType === 'image_output' ? (
|
||||
{template.outputType === 'image_output' ? (
|
||||
nes.outputs.map((result, i) => (
|
||||
<ImageOutputPreview key={getKey(result, i)} output={result as ImageOutput} />
|
||||
))
|
||||
@@ -61,8 +59,5 @@ const InspectorOutputsTab = () => {
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InspectorOutputsTab);
|
||||
|
||||
const getKey = (result: AnyInvocationOutput, i: number) => `${result.type}-${i}`;
|
||||
});
|
||||
Content.displayName = 'Content';
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { TemplateGate } from 'features/nodes/components/sidePanel/inspector/NodeTemplateGate';
|
||||
import { useNodeTemplate } from 'features/nodes/hooks/useNodeTemplate';
|
||||
import { selectLastSelectedNodeId } from 'features/nodes/store/selectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const NodeTemplateInspector = () => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createMemoizedSelector(selectNodesSlice, (nodes) => {
|
||||
const lastSelectedNode = selectLastSelectedNode(nodes);
|
||||
const lastSelectedNodeTemplate = lastSelectedNode ? templates[lastSelectedNode.data.type] : undefined;
|
||||
|
||||
return lastSelectedNodeTemplate;
|
||||
}),
|
||||
[templates]
|
||||
);
|
||||
const template = useAppSelector(selector);
|
||||
const lastSelectedNodeId = useAppSelector(selectLastSelectedNodeId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!template) {
|
||||
if (!lastSelectedNodeId) {
|
||||
return <IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />;
|
||||
}
|
||||
|
||||
return <DataViewer data={template} label={t('nodes.nodeTemplate')} />;
|
||||
return (
|
||||
<TemplateGate
|
||||
nodeId={lastSelectedNodeId}
|
||||
fallback={<IAINoContentFallback label={t('nodes.noNodeSelected')} icon={null} />}
|
||||
>
|
||||
<Content nodeId={lastSelectedNodeId} />
|
||||
</TemplateGate>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NodeTemplateInspector);
|
||||
|
||||
const Content = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const template = useNodeTemplate(nodeId);
|
||||
|
||||
return <DataViewer data={template} label={t('nodes.nodeTemplate')} />;
|
||||
});
|
||||
Content.displayName = 'Content';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useNodeTemplateSafe } from 'features/nodes/hooks/useNodeTemplate';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const TemplateGate = memo(
|
||||
({ nodeId, fallback, children }: PropsWithChildren<{ nodeId: string; fallback: ReactNode }>) => {
|
||||
const template = useNodeTemplateSafe(nodeId);
|
||||
|
||||
if (!template) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
);
|
||||
TemplateGate.displayName = 'TemplateGate';
|
||||
@@ -15,3 +15,10 @@ export const useNodeTemplate = (nodeId: string): InvocationTemplate => {
|
||||
}, [templates, type]);
|
||||
return template;
|
||||
};
|
||||
|
||||
export const useNodeTemplateSafe = (nodeId: string): InvocationTemplate | null => {
|
||||
const templates = useStore($templates);
|
||||
const type = useNodeType(nodeId);
|
||||
const template = useMemo(() => templates[type] ?? null, [templates, type]);
|
||||
return template;
|
||||
};
|
||||
|
||||
@@ -56,6 +56,14 @@ export const selectLastSelectedNode = (nodesSlice: NodesState) => {
|
||||
|
||||
export const selectNodesSlice = (state: RootState) => state.nodes.present;
|
||||
|
||||
export const selectLastSelectedNodeId = createSelector(selectNodesSlice, ({ nodes }) => {
|
||||
const selectedNodes = nodes.filter(isInvocationNode).filter((n) => n.selected);
|
||||
if (selectedNodes.length === 1) {
|
||||
return selectedNodes[0]?.id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const createNodesSelector = <T>(selector: Selector<NodesState, T>) => createSelector(selectNodesSlice, selector);
|
||||
export const selectNodes = createNodesSelector((nodes) => nodes.nodes);
|
||||
export const selectEdges = createNodesSelector((nodes) => nodes.edges);
|
||||
|
||||
Reference in New Issue
Block a user