feat(ui): add Update All Nodes button

This commit is contained in:
psychedelicious 2023-11-16 12:42:25 +11:00
parent 3f6e8e9d6b
commit 7fcf475aec
8 changed files with 183 additions and 46 deletions

View File

@ -920,7 +920,10 @@
"unknownTemplate": "Unknown Template",
"unkownInvocation": "Unknown Invocation type",
"updateNode": "Update Node",
"updateAllNodes": "Update All Nodes",
"updateApp": "Update App",
"unableToUpdateNodes_one": "Unable to update {{count}} node",
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
"vaeField": "Vae",
"vaeFieldDescription": "Vae submodel.",
"vaeModelField": "VAE",

View File

@ -72,6 +72,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa
import { addTabChangedListener } from './listeners/tabChanged';
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
import { addWorkflowLoadedListener } from './listeners/workflowLoaded';
import { addUpdateAllNodesRequestedListener } from './listeners/updateAllNodesRequested';
export const listenerMiddleware = createListenerMiddleware();
@ -178,6 +179,7 @@ addReceivedOpenAPISchemaListener();
// Workflows
addWorkflowLoadedListener();
addUpdateAllNodesRequestedListener();
// DND
addImageDroppedListener();

View File

@ -0,0 +1,52 @@
import {
getNeedsUpdate,
updateNode,
} from 'features/nodes/hooks/useNodeVersion';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
import { nodeReplaced } from 'features/nodes/store/nodesSlice';
import { startAppListening } from '..';
import { logger } from 'app/logging/logger';
import { addToast } from 'features/system/store/systemSlice';
import { makeToast } from 'features/system/util/makeToast';
import { t } from 'i18next';
export const addUpdateAllNodesRequestedListener = () => {
startAppListening({
actionCreator: updateAllNodesRequested,
effect: (action, { dispatch, getState }) => {
const log = logger('nodes');
const nodes = getState().nodes.nodes;
const templates = getState().nodes.nodeTemplates;
let unableToUpdateCount = 0;
nodes.forEach((node) => {
const template = templates[node.data.type];
const needsUpdate = getNeedsUpdate(node, template);
const updatedNode = updateNode(node, template);
if (!updatedNode) {
if (needsUpdate) {
unableToUpdateCount++;
}
return;
}
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
});
if (unableToUpdateCount) {
log.warn(
`Unable to update ${unableToUpdateCount} nodes. Please report this issue.`
);
dispatch(
addToast(
makeToast({
title: t('nodes.unableToUpdateNodes', {
count: unableToUpdateCount,
}),
})
)
);
}
},
});
};

View File

@ -3,15 +3,22 @@ import { useAppDispatch } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
import { memo, useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { FaPlus, FaSync } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import IAIButton from 'common/components/IAIButton';
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
import { updateAllNodesRequested } from 'features/nodes/store/actions';
const TopLeftPanel = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const nodesNeedUpdate = useGetNodesNeedUpdate();
const handleOpenAddNodePopover = useCallback(() => {
dispatch(addNodePopoverOpened());
}, [dispatch]);
const handleClickUpdateNodes = useCallback(() => {
dispatch(updateAllNodesRequested());
}, [dispatch]);
return (
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
@ -21,6 +28,11 @@ const TopLeftPanel = () => {
icon={<FaPlus />}
onClick={handleOpenAddNodePopover}
/>
{nodesNeedUpdate && (
<IAIButton leftIcon={<FaSync />} onClick={handleClickUpdateNodes}>
{t('nodes.updateAllNodes')}
</IAIButton>
)}
</Flex>
);
};

View File

@ -107,7 +107,7 @@ const Content = (props: {
{props.node.data.version}
</Text>
</FormControl>
{mayUpdate && (
{needsUpdate && (
<IAIIconButton
aria-label={t('nodes.updateNode')}
tooltip={t('nodes.updateNode')}

View File

@ -0,0 +1,25 @@
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { getNeedsUpdate } from './useNodeVersion';
const selector = createSelector(
stateSelector,
(state) => {
const nodes = state.nodes.nodes;
const templates = state.nodes.nodeTemplates;
const needsUpdate = nodes.some((node) => {
const template = templates[node.data.type];
return getNeedsUpdate(node, template);
});
return needsUpdate;
},
defaultSelectorOptions
);
export const useGetNodesNeedUpdate = () => {
const getNeedsUpdate = useAppSelector(selector);
return getNeedsUpdate;
};

View File

@ -3,20 +3,80 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { satisfies } from 'compare-versions';
import { cloneDeep, defaultsDeep } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { Node } from 'reactflow';
import { AnyInvocationType } from 'services/events/types';
import { nodeReplaced } from '../store/nodesSlice';
import { buildNodeData } from '../store/util/buildNodeData';
import {
InvocationNodeData,
InvocationTemplate,
NodeData,
isInvocationNode,
zParsedSemver,
} from '../types/types';
import { cloneDeep, defaultsDeep } from 'lodash-es';
import { buildNodeData } from '../store/util/buildNodeData';
import { AnyInvocationType } from 'services/events/types';
import { Node } from 'reactflow';
import { nodeReplaced } from '../store/nodesSlice';
import { useAppToaster } from 'app/components/Toaster';
import { useTranslation } from 'react-i18next';
export const getNeedsUpdate = (
node?: Node<NodeData>,
template?: InvocationTemplate
) => {
if (!isInvocationNode(node) || !template) {
return false;
}
return node.data.version !== template.version;
};
export const getMayUpdateNode = (
node?: Node<NodeData>,
template?: InvocationTemplate
) => {
const needsUpdate = getNeedsUpdate(node, template);
if (
!needsUpdate ||
!isInvocationNode(node) ||
!template ||
!node.data.version
) {
return false;
}
const templateMajor = zParsedSemver.parse(template.version).major;
return satisfies(node.data.version, `^${templateMajor}`);
};
export const updateNode = (
node?: Node<NodeData>,
template?: InvocationTemplate
) => {
const mayUpdate = getMayUpdateNode(node, template);
if (
!mayUpdate ||
!isInvocationNode(node) ||
!template ||
!node.data.version
) {
return;
}
const defaults = buildNodeData(
node.data.type as AnyInvocationType,
node.position,
template
) as Node<InvocationNodeData>;
const clone = cloneDeep(node);
clone.data.version = template.version;
defaultsDeep(clone, defaults);
return clone;
};
export const useNodeVersion = (nodeId: string) => {
const dispatch = useAppDispatch();
const toast = useAppToaster();
const { t } = useTranslation();
const selector = useMemo(
() =>
createSelector(
@ -33,48 +93,27 @@ export const useNodeVersion = (nodeId: string) => {
const { node, nodeTemplate } = useAppSelector(selector);
const needsUpdate = useMemo(() => {
if (!isInvocationNode(node) || !nodeTemplate) {
return false;
}
return node.data.version !== nodeTemplate.version;
}, [node, nodeTemplate]);
const needsUpdate = useMemo(
() => getNeedsUpdate(node, nodeTemplate),
[node, nodeTemplate]
);
const mayUpdate = useMemo(() => {
if (
!needsUpdate ||
!isInvocationNode(node) ||
!nodeTemplate ||
!node.data.version
) {
return false;
}
const templateMajor = zParsedSemver.parse(nodeTemplate.version).major;
const mayUpdate = useMemo(
() => getMayUpdateNode(node, nodeTemplate),
[node, nodeTemplate]
);
return satisfies(node.data.version, `^${templateMajor}`);
}, [needsUpdate, node, nodeTemplate]);
const updateNode = useCallback(() => {
if (
!mayUpdate ||
!isInvocationNode(node) ||
!nodeTemplate ||
!node.data.version
) {
const _updateNode = useCallback(() => {
const needsUpdate = getNeedsUpdate(node, nodeTemplate);
const updatedNode = updateNode(node, nodeTemplate);
if (!updatedNode) {
if (needsUpdate) {
toast({ title: t('nodes.unableToUpdateNodes', { count: 1 }) });
}
return;
}
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
}, [dispatch, node, nodeTemplate, t, toast]);
const defaults = buildNodeData(
node.data.type as AnyInvocationType,
node.position,
nodeTemplate
) as Node<InvocationNodeData>;
const clone = cloneDeep(node);
clone.data.version = nodeTemplate.version;
defaultsDeep(clone, defaults);
dispatch(nodeReplaced({ nodeId: clone.id, node: clone }));
}, [dispatch, mayUpdate, node, nodeTemplate]);
return { needsUpdate, mayUpdate, updateNode };
return { needsUpdate, mayUpdate, updateNode: _updateNode };
};

View File

@ -21,3 +21,7 @@ export const isAnyGraphBuilt = isAnyOf(
export const workflowLoadRequested = createAction<Workflow>(
'nodes/workflowLoadRequested'
);
export const updateAllNodesRequested = createAction(
'nodes/updateAllNodesRequested'
);