refactor(ui): migrate from canceling queue items to deleteing, make queue hook APIs consistent

This commit is contained in:
psychedelicious
2025-06-06 18:29:43 +10:00
parent cc5083599d
commit 2ddcde13ff
50 changed files with 773 additions and 516 deletions

View File

@@ -14,6 +14,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByBatchIDsResult,
CancelByDestinationResult,
ClearResult,
DeleteAllExceptCurrentResult,
DeleteByDestinationResult,
EnqueueBatchResult,
FieldIdentifier,
@@ -146,6 +147,18 @@ async def cancel_all_except_current(
return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id)
@session_queue_router.put(
"/{queue_id}/delete_all_except_current",
operation_id="delete_all_except_current",
responses={200: {"model": DeleteAllExceptCurrentResult}},
)
async def delete_all_except_current(
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> DeleteAllExceptCurrentResult:
"""Immediately deletes all queue items except in-processing items"""
return ApiDependencies.invoker.services.session_queue.delete_all_except_current(queue_id=queue_id)
@session_queue_router.put(
"/{queue_id}/cancel_by_batch_ids",
operation_id="cancel_by_batch_ids",

View File

@@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
DeleteAllExceptCurrentResult,
DeleteByDestinationResult,
EnqueueBatchResult,
IsEmptyResult,
@@ -129,6 +130,11 @@ class SessionQueueBase(ABC):
"""Cancels all queue items except in-progress items"""
pass
@abstractmethod
def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult:
"""Deletes all queue items except in-progress items"""
pass
@abstractmethod
def list_queue_items(
self,

View File

@@ -369,6 +369,12 @@ class DeleteByDestinationResult(BaseModel):
deleted: int = Field(..., description="Number of queue items deleted")
class DeleteAllExceptCurrentResult(DeleteByDestinationResult):
"""Result of deleting all except current"""
pass
class CancelByQueueIDResult(CancelByBatchIDsResult):
"""Result of canceling by queue id"""

View File

@@ -17,6 +17,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
CancelByDestinationResult,
CancelByQueueIDResult,
ClearResult,
DeleteAllExceptCurrentResult,
DeleteByDestinationResult,
EnqueueBatchResult,
IsEmptyResult,
@@ -489,6 +490,37 @@ class SqliteSessionQueue(SessionQueueBase):
raise
return DeleteByDestinationResult(deleted=count)
def delete_all_except_current(self, queue_id: str) -> DeleteAllExceptCurrentResult:
try:
cursor = self._conn.cursor()
where = """--sql
WHERE
queue_id == ?
AND status == 'pending'
"""
cursor.execute(
f"""--sql
SELECT COUNT(*)
FROM session_queue
{where};
""",
(queue_id,),
)
count = cursor.fetchone()[0]
cursor.execute(
f"""--sql
DELETE
FROM session_queue
{where};
""",
(queue_id,),
)
self._conn.commit()
except Exception:
self._conn.rollback()
raise
return DeleteAllExceptCurrentResult(deleted=count)
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
try:
cursor = self._conn.cursor()

View File

@@ -16,6 +16,7 @@ import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow
import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal';
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
import RefreshAfterResetModal from 'features/system/components/SettingsModal/RefreshAfterResetModal';
@@ -40,6 +41,7 @@ export const GlobalModalIsolator = memo(() => {
<StylePresetModal />
<WorkflowLibraryModal />
<CancelAllExceptCurrentQueueItemConfirmationAlertDialog />
<DeleteAllExceptCurrentQueueItemConfirmationAlertDialog />
<ClearQueueConfirmationsAlertDialog />
<NewWorkflowConfirmationAlertDialog />
<LoadWorkflowConfirmationAlertDialog />

View File

@@ -1,6 +1,6 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -35,34 +35,30 @@ export const useGlobalHotkeys = () => {
dependencies: [queue],
});
const {
cancelQueueItem,
isDisabled: isDisabledCancelQueueItem,
isLoading: isLoadingCancelQueueItem,
} = useCancelCurrentQueueItem();
const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
useRegisteredHotkeys({
id: 'cancelQueueItem',
category: 'app',
callback: cancelQueueItem,
callback: deleteCurrentQueueItem.trigger,
options: {
enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
enabled: !deleteCurrentQueueItem.isDisabled && !deleteCurrentQueueItem.isLoading,
preventDefault: true,
},
dependencies: [cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem],
dependencies: [deleteCurrentQueueItem],
});
const { clearQueue, isDisabled: isDisabledClearQueue, isLoading: isLoadingClearQueue } = useClearQueue();
const clearQueue = useClearQueue();
useRegisteredHotkeys({
id: 'clearQueue',
category: 'app',
callback: clearQueue,
callback: clearQueue.trigger,
options: {
enabled: !isDisabledClearQueue && !isLoadingClearQueue,
enabled: !clearQueue.isDisabled && !clearQueue.isLoading,
preventDefault: true,
},
dependencies: [clearQueue, isDisabledClearQueue, isLoadingClearQueue],
dependencies: [clearQueue],
});
useRegisteredHotkeys({

View File

@@ -67,34 +67,6 @@ const setProgress = ($progressData: WritableAtom<Record<number, ProgressData>>,
}
};
const clearProgressEvent = ($progressData: WritableAtom<Record<number, ProgressData>>, itemId: number) => {
const progressData = $progressData.get();
const current = progressData[itemId];
if (!current) {
return;
}
const next = { ...current };
next.progressEvent = null;
$progressData.set({
...progressData,
[itemId]: next,
});
};
const clearProgressImage = ($progressData: WritableAtom<Record<number, ProgressData>>, itemId: number) => {
const progressData = $progressData.get();
const current = progressData[itemId];
if (!current) {
return;
}
const next = { ...current };
next.progressImage = null;
$progressData.set({
...progressData,
[itemId]: next,
});
};
type CanvasSessionContextValue = {
session: { id: string; type: 'simple' | 'advanced' };
$items: Atom<S['SessionQueueItem'][]>;
@@ -132,15 +104,11 @@ export const CanvasSessionContextProvider = memo(
const socket = useStore($socket);
/**
* Manually-synced atom containing the queue items for the current session.
* Manually-synced atom containing queue items for the current session. This is populated from the RTK Query cache
* and kept in sync with it via a redux subscription.
*/
const $items = useState(() => atom<S['SessionQueueItem'][]>([]))[0];
/**
* Manually-synced atom containing the queue items for the current session.
*/
const $prevItems = useState(() => atom<S['SessionQueueItem'][]>([]))[0];
/**
* Whether auto-switch is enabled.
*/
@@ -294,28 +262,16 @@ export const CanvasSessionContextProvider = memo(
setProgress($progressData, data);
};
const onQueueItemStatusChanged = (data: S['QueueItemStatusChangedEvent']) => {
if (data.destination !== session.id) {
return;
}
if (data.status === 'canceled' || data.status === 'failed') {
clearProgressEvent($progressData, data.item_id);
clearProgressImage($progressData, data.item_id);
}
};
socket.on('invocation_progress', onProgress);
socket.on('queue_item_status_changed', onQueueItemStatusChanged);
return () => {
socket.off('invocation_progress', onProgress);
socket.off('queue_item_status_changed', onQueueItemStatusChanged);
};
}, [$autoSwitch, $progressData, $selectedItemId, session.id, socket]);
// Set up state subscriptions and effects
useEffect(() => {
let _prevItems: readonly S['SessionQueueItem'][] = [];
// Seed the $items atom with the initial query cache state
$items.set(selectQueueItems(store.getState()));
@@ -324,7 +280,7 @@ export const CanvasSessionContextProvider = memo(
const prevItems = $items.get();
const items = selectQueueItems(store.getState());
if (items !== prevItems) {
$prevItems.set(prevItems);
_prevItems = prevItems;
$items.set(items);
}
});
@@ -344,7 +300,7 @@ export const CanvasSessionContextProvider = memo(
// If an item is selected and it is not in the list of items, un-set it. This effect will run again and we'll
// the above case, selecting the first item if there are any.
if (selectedItemId !== null && items.findIndex(({ item_id }) => item_id === selectedItemId) === -1) {
let prevIndex = $prevItems.get().findIndex(({ item_id }) => item_id === selectedItemId);
let prevIndex = _prevItems.findIndex(({ item_id }) => item_id === selectedItemId);
if (prevIndex >= items.length) {
prevIndex = items.length - 1;
}
@@ -357,19 +313,37 @@ export const CanvasSessionContextProvider = memo(
// Clean up the progress data when a queue item is discarded.
const unsubCleanUpProgressData = $items.listen((items) => {
const progressData = $progressData.get();
const toDelete: number[] = [];
const toClear: number[] = [];
for (const datum of Object.values(progressData)) {
if (items.findIndex(({ item_id }) => item_id === datum.itemId) === -1) {
const item = items.find(({ item_id }) => item_id === datum.itemId);
if (!item) {
toDelete.push(datum.itemId);
} else if (item.status === 'canceled' || item.status === 'failed') {
toClear.push(datum.itemId);
}
}
if (toDelete.length === 0) {
return;
}
const newProgressData = { ...progressData };
for (const itemId of toDelete) {
delete newProgressData[itemId];
}
for (const itemId of toClear) {
const current = newProgressData[itemId];
if (current) {
current.progressEvent = null;
current.progressImage = null;
}
}
$progressData.set(newProgressData);
});
@@ -408,17 +382,7 @@ export const CanvasSessionContextProvider = memo(
$progressData.set({});
$selectedItemId.set(null);
};
}, [
$autoSwitch,
$items,
$lastLoadedItemId,
$prevItems,
$progressData,
$selectedItemId,
selectQueueItems,
session.id,
store,
]);
}, [$autoSwitch, $items, $lastLoadedItemId, $progressData, $selectedItemId, selectQueueItems, session.id, store]);
const value = useMemo<CanvasSessionContextValue>(
() => ({

View File

@@ -9,11 +9,11 @@ import { canvasSessionGenerationFinished } from 'features/controlLayers/store/ca
import { selectBboxRect, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageNameToImageObject } from 'features/controlLayers/store/util';
import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination';
import { memo, useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { PiCheckBold } from 'react-icons/pi';
import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarAcceptButton = memo(() => {
const ctx = useCanvasSessionContext();
@@ -24,7 +24,7 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
const isCanvasFocused = useIsRegionFocused('canvas');
const selectedItemImageName = useStore(ctx.$selectedItemOutputImageName);
const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
const deleteQueueItemsByDestination = useDeleteQueueItemsByDestination();
const { t } = useTranslation();
@@ -41,8 +41,15 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
dispatch(rasterLayerAdded({ overrides, isSelected: selectedEntityIdentifier?.type === 'raster_layer' }));
dispatch(canvasSessionGenerationFinished());
deleteByDestination({ destination: ctx.session.id });
}, [selectedItemImageName, bboxRect, dispatch, selectedEntityIdentifier?.type, deleteByDestination, ctx.session.id]);
deleteQueueItemsByDestination.trigger(ctx.session.id);
}, [
selectedItemImageName,
bboxRect,
dispatch,
selectedEntityIdentifier?.type,
deleteQueueItemsByDestination,
ctx.session.id,
]);
useHotkeys(
['enter'],
@@ -61,7 +68,8 @@ export const StagingAreaToolbarAcceptButton = memo(() => {
icon={<PiCheckBold />}
onClick={acceptSelected}
colorScheme="invokeBlue"
isDisabled={!selectedItemImageName || !shouldShowStagedImage}
isDisabled={!selectedItemImageName || !shouldShowStagedImage || deleteQueueItemsByDestination.isDisabled}
isLoading={deleteQueueItemsByDestination.isLoading}
/>
);
});

View File

@@ -2,21 +2,21 @@ import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteQueueItemsByDestination } from 'features/queue/hooks/useDeleteQueueItemsByDestination';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleBold } from 'react-icons/pi';
import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const ctx = useCanvasSessionContext();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const [deleteByDestination] = useDeleteQueueItemsByDestinationMutation();
const deleteQueueItemsByDestination = useDeleteQueueItemsByDestination();
const discardAll = useCallback(() => {
deleteByDestination({ destination: ctx.session.id });
deleteQueueItemsByDestination.trigger(ctx.session.id);
dispatch(canvasSessionGenerationFinished());
}, [deleteByDestination, ctx.session.id, dispatch]);
}, [deleteQueueItemsByDestination, ctx.session.id, dispatch]);
return (
<IconButton
@@ -26,7 +26,8 @@ export const StagingAreaToolbarDiscardAllButton = memo(({ isDisabled }: { isDisa
onClick={discardAll}
colorScheme="error"
fontSize={16}
isDisabled={isDisabled}
isDisabled={isDisabled || deleteQueueItemsByDestination.isDisabled}
isLoading={deleteQueueItemsByDestination.isLoading}
/>
);
});

View File

@@ -3,15 +3,15 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context';
import { canvasSessionGenerationFinished } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { isDisabled?: boolean }) => {
const dispatch = useAppDispatch();
const ctx = useCanvasSessionContext();
const [deleteQueueItem] = useDeleteQueueItemMutation();
const deleteQueueItem = useDeleteQueueItem();
const selectedItemId = useStore(ctx.$selectedItemId);
const { t } = useTranslation();
@@ -21,10 +21,10 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
return;
}
const itemCount = ctx.$itemCount.get();
deleteQueueItem({ item_id: selectedItemId });
if (itemCount <= 1) {
dispatch(canvasSessionGenerationFinished());
}
deleteQueueItem.trigger(selectedItemId);
}, [selectedItemId, ctx.$itemCount, deleteQueueItem, dispatch]);
return (
@@ -35,7 +35,8 @@ export const StagingAreaToolbarDiscardSelectedButton = memo(({ isDisabled }: { i
onClick={discardSelected}
colorScheme="invokeBlue"
fontSize={16}
isDisabled={selectedItemId === null || isDisabled}
isDisabled={selectedItemId === null || deleteQueueItem.isDisabled || isDisabled}
isLoading={deleteQueueItem.isLoading}
/>
);
});

View File

@@ -0,0 +1,25 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXCircle } from 'react-icons/pi';
export const CancelAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
return (
<IconButton
size="lg"
isDisabled={cancelAllExceptCurrent.isDisabled}
isLoading={cancelAllExceptCurrent.isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.cancelAllExceptCurrentTooltip')}
icon={<PiXCircle />}
colorScheme="error"
onClick={cancelAllExceptCurrent.openDialog}
/>
);
});
CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';

View File

@@ -9,16 +9,15 @@ const [useCancelAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBool
export const useCancelAllExceptCurrentQueueItemDialog = () => {
const dialog = useCancelAllExceptCurrentQueueItemConfirmationAlertDialog();
const { cancelAllExceptCurrentQueueItem, isLoading, isDisabled, queueStatus } = useCancelAllExceptCurrentQueueItem();
const cancelAllExceptCurrentQueueItem = useCancelAllExceptCurrentQueueItem();
return {
cancelAllExceptCurrentQueueItem,
trigger: cancelAllExceptCurrentQueueItem.trigger,
isOpen: dialog.isTrue,
openDialog: dialog.setTrue,
closeDialog: dialog.setFalse,
isLoading,
queueStatus,
isDisabled,
isLoading: cancelAllExceptCurrentQueueItem.isLoading,
isDisabled: cancelAllExceptCurrentQueueItem.isDisabled,
};
};
@@ -32,7 +31,7 @@ export const CancelAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() =>
isOpen={cancelAllExceptCurrentQueueItem.isOpen}
onClose={cancelAllExceptCurrentQueueItem.closeDialog}
title={t('queue.cancelAllExceptCurrentTooltip')}
acceptCallback={cancelAllExceptCurrentQueueItem.cancelAllExceptCurrentQueueItem}
acceptCallback={cancelAllExceptCurrentQueueItem.trigger}
acceptButtonText={t('queue.confirm')}
useInert={false}
>

View File

@@ -5,10 +5,14 @@ import { useTranslation } from 'react-i18next';
const ClearInvocationCacheButton = () => {
const { t } = useTranslation();
const { clearInvocationCache, isDisabled, isLoading } = useClearInvocationCache();
const clearInvocationCache = useClearInvocationCache();
return (
<Button isDisabled={isDisabled} isLoading={isLoading} onClick={clearInvocationCache}>
<Button
onClick={clearInvocationCache.trigger}
isDisabled={clearInvocationCache.isDisabled}
isLoading={clearInvocationCache.isLoading}
>
{t('invocationCache.clear')}
</Button>
);

View File

@@ -9,16 +9,15 @@ const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false);
const useClearQueueDialog = () => {
const dialog = useClearQueueConfirmationAlertDialog();
const { clearQueue, isLoading, isDisabled, queueStatus } = useClearQueue();
const clearQueue = useClearQueue();
return {
clearQueue,
isOpen: dialog.isTrue,
openDialog: dialog.setTrue,
closeDialog: dialog.setFalse,
isLoading,
queueStatus,
isDisabled,
trigger: clearQueue.trigger,
isLoading: clearQueue.isLoading,
isDisabled: clearQueue.isDisabled,
};
};
@@ -32,7 +31,7 @@ export const ClearQueueConfirmationsAlertDialog = memo(() => {
isOpen={clearQueue.isOpen}
onClose={clearQueue.closeDialog}
title={t('queue.clearTooltip')}
acceptCallback={clearQueue.clearQueue}
acceptCallback={clearQueue.trigger}
acceptButtonText={t('queue.clear')}
useInert={false}
>

View File

@@ -1,58 +0,0 @@
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold, PiXCircle } from 'react-icons/pi';
export const ClearQueueIconButton = memo(() => {
const shift = useShiftModifier();
if (!shift) {
return <CancelCurrentIconButton />;
}
return <CancelAllExceptCurrentIconButton />;
});
ClearQueueIconButton.displayName = 'ClearQueueIconButton';
const CancelCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
return (
<IconButton
size="lg"
isDisabled={cancelCurrentQueueItem.isDisabled}
isLoading={cancelCurrentQueueItem.isLoading}
aria-label={t('queue.cancel')}
tooltip={t('queue.cancelTooltip')}
icon={<PiXBold />}
colorScheme="error"
onClick={cancelCurrentQueueItem.cancelQueueItem}
/>
);
});
CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
const CancelAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
return (
<IconButton
size="lg"
isDisabled={cancelAllExceptCurrent.isDisabled}
isLoading={cancelAllExceptCurrent.isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.cancelAllExceptCurrentTooltip')}
icon={<PiXCircle />}
colorScheme="error"
onClick={cancelAllExceptCurrent.openDialog}
/>
);
});
CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';

View File

@@ -0,0 +1,46 @@
import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { useDeleteAllExceptCurrentQueueItem } from 'features/queue/hooks/useDeleteAllExceptCurrentQueueItem';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const [useDeleteAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBoolean(false);
export const useDeleteAllExceptCurrentQueueItemDialog = () => {
const dialog = useDeleteAllExceptCurrentQueueItemConfirmationAlertDialog();
const deleteAllExceptCurrentQueueItem = useDeleteAllExceptCurrentQueueItem();
return {
trigger: deleteAllExceptCurrentQueueItem.trigger,
isOpen: dialog.isTrue,
openDialog: dialog.setTrue,
closeDialog: dialog.setFalse,
isLoading: deleteAllExceptCurrentQueueItem.isLoading,
isDisabled: deleteAllExceptCurrentQueueItem.isDisabled,
};
};
export const DeleteAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() => {
useAssertSingleton('DeleteAllExceptCurrentQueueItemConfirmationAlertDialog');
const { t } = useTranslation();
const deleteAllExceptCurrentQueueItem = useDeleteAllExceptCurrentQueueItemDialog();
return (
<ConfirmationAlertDialog
isOpen={deleteAllExceptCurrentQueueItem.isOpen}
onClose={deleteAllExceptCurrentQueueItem.closeDialog}
title={t('queue.cancelAllExceptCurrentTooltip')}
acceptCallback={deleteAllExceptCurrentQueueItem.trigger}
acceptButtonText={t('queue.confirm')}
useInert={false}
>
<Text>{t('queue.cancelAllExceptCurrentQueueItemAlertDialog')}</Text>
<br />
<Text>{t('queue.cancelAllExceptCurrentQueueItemAlertDialog2')}</Text>
</ConfirmationAlertDialog>
);
});
DeleteAllExceptCurrentQueueItemConfirmationAlertDialog.displayName =
'DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';

View File

@@ -0,0 +1,25 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
export const DeleteCurrentQueueItemIconButton = memo(() => {
const { t } = useTranslation();
const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
return (
<IconButton
size="lg"
onClick={deleteCurrentQueueItem.trigger}
isDisabled={deleteCurrentQueueItem.isDisabled}
isLoading={deleteCurrentQueueItem.isLoading}
aria-label={t('queue.cancel')}
tooltip={t('queue.cancelTooltip')}
icon={<PiXBold />}
colorScheme="error"
/>
);
});
DeleteCurrentQueueItemIconButton.displayName = 'DeleteCurrentQueueItemIconButton';

View File

@@ -11,17 +11,17 @@ type Props = {
const PauseProcessorButton = ({ asIconButton }: Props) => {
const { t } = useTranslation();
const { pauseProcessor, isLoading, isDisabled } = usePauseProcessor();
const pauseProcessor = usePauseProcessor();
return (
<QueueButton
asIconButton={asIconButton}
label={t('queue.pause')}
tooltip={t('queue.pauseTooltip')}
isDisabled={isDisabled}
isLoading={isLoading}
isDisabled={pauseProcessor.isDisabled}
isLoading={pauseProcessor.isLoading}
icon={<PiPauseFill />}
onClick={pauseProcessor}
onClick={pauseProcessor.trigger}
colorScheme="gold"
/>
);

View File

@@ -1,4 +1,4 @@
import { usePruneQueue } from 'features/queue/hooks/usePruneQueue';
import { useFinishedCount, usePruneQueue } from 'features/queue/hooks/usePruneQueue';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiBroomBold } from 'react-icons/pi';
@@ -11,17 +11,18 @@ type Props = {
const PruneQueueButton = ({ asIconButton }: Props) => {
const { t } = useTranslation();
const { pruneQueue, isLoading, finishedCount, isDisabled } = usePruneQueue();
const pruneQueue = usePruneQueue();
const finishedCount = useFinishedCount();
return (
<QueueButton
isDisabled={isDisabled}
isLoading={isLoading}
onClick={pruneQueue.trigger}
isDisabled={pruneQueue.isDisabled}
isLoading={pruneQueue.isLoading}
asIconButton={asIconButton}
label={t('queue.prune')}
tooltip={t('queue.pruneTooltip', { item_count: finishedCount })}
icon={<PiBroomBold />}
onClick={pruneQueue}
colorScheme="invokeBlue"
/>
);

View File

@@ -1,9 +1,9 @@
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { SessionMenuItems } from 'common/components/SessionMenuItems';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -18,18 +18,10 @@ export const QueueActionsMenuButton = memo(() => {
const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
const cancelCurrent = useCancelCurrentQueueItem();
const {
resumeProcessor,
isLoading: isLoadingResumeProcessor,
isDisabled: isDisabledResumeProcessor,
} = useResumeProcessor();
const {
pauseProcessor,
isLoading: isLoadingPauseProcessor,
isDisabled: isDisabledPauseProcessor,
} = usePauseProcessor();
const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
const resumeProcessor = useResumeProcessor();
const pauseProcessor = usePauseProcessor();
const openQueue = useCallback(() => {
dispatch(setActiveTab('queue'));
}, [dispatch]);
@@ -46,27 +38,27 @@ export const QueueActionsMenuButton = memo(() => {
<MenuItem
isDestructive
icon={<PiXBold />}
onClick={cancelCurrent.cancelQueueItem}
isLoading={cancelCurrent.isLoading}
isDisabled={cancelCurrent.isDisabled}
onClick={deleteCurrentQueueItem.trigger}
isLoading={deleteCurrentQueueItem.isLoading}
isDisabled={deleteCurrentQueueItem.isDisabled}
>
{t('queue.cancelTooltip')}
</MenuItem>
<MenuItem
isDestructive
icon={<PiXCircle />}
onClick={cancelAllExceptCurrent.openDialog}
isLoading={cancelAllExceptCurrent.isLoading}
isDisabled={cancelAllExceptCurrent.isDisabled}
onClick={deleteAllExceptCurrent.openDialog}
isLoading={deleteAllExceptCurrent.isLoading}
isDisabled={deleteAllExceptCurrent.isDisabled}
>
{t('queue.cancelAllExceptCurrentTooltip')}
</MenuItem>
{isResumeEnabled && (
<MenuItem
icon={<PiPlayFill />}
onClick={resumeProcessor}
isLoading={isLoadingResumeProcessor}
isDisabled={isDisabledResumeProcessor}
onClick={resumeProcessor.trigger}
isLoading={resumeProcessor.isLoading}
isDisabled={resumeProcessor.isDisabled}
>
{t('queue.resumeTooltip')}
</MenuItem>
@@ -74,9 +66,9 @@ export const QueueActionsMenuButton = memo(() => {
{isPauseEnabled && (
<MenuItem
icon={<PiPauseFill />}
onClick={pauseProcessor}
isLoading={isLoadingPauseProcessor}
isDisabled={isDisabledPauseProcessor}
onClick={pauseProcessor.trigger}
isLoading={pauseProcessor.isLoading}
isDisabled={pauseProcessor.isDisabled}
>
{t('queue.pauseTooltip')}
</MenuItem>

View File

@@ -1,5 +1,6 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { ClearQueueIconButton } from 'features/queue/components/ClearQueueIconButton';
import { Flex, Spacer, useShiftModifier } from '@invoke-ai/ui-library';
import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { DeleteCurrentQueueItemIconButton } from 'features/queue/components/DeleteCurrentQueueItemIconButton';
import { QueueActionsMenuButton } from 'features/queue/components/QueueActionsMenuButton';
import ProgressBar from 'features/system/components/ProgressBar';
import { memo } from 'react';
@@ -13,7 +14,7 @@ const QueueControls = () => {
<InvokeButton />
<Spacer />
<QueueActionsMenuButton />
<ClearQueueIconButton />
<DeleteIconButton />
</Flex>
<ProgressBar />
</Flex>
@@ -21,3 +22,15 @@ const QueueControls = () => {
};
export default memo(QueueControls);
export const DeleteIconButton = memo(() => {
const shift = useShiftModifier();
if (!shift) {
return <DeleteCurrentQueueItemIconButton />;
}
return <DeleteAllExceptCurrentQueueItemConfirmationAlertDialog />;
});
DeleteIconButton.displayName = 'DeleteIconButton';

View File

@@ -3,7 +3,7 @@ import { Badge, ButtonGroup, Collapse, Flex, IconButton, Text } from '@invoke-ai
import QueueStatusBadge from 'features/queue/components/common/QueueStatusBadge';
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem';
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@@ -38,21 +38,21 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
const handleToggle = useCallback(() => {
context.toggleQueueItem(item.item_id);
}, [context, item.item_id]);
const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item.item_id);
const handleCancelQueueItem = useCallback(
const deleteQueueItem = useDeleteQueueItem();
const onClickDeleteQueueItem = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
cancelQueueItem();
deleteQueueItem.trigger(item.item_id);
},
[cancelQueueItem]
[deleteQueueItem, item.item_id]
);
const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item.item_id);
const handleRetryQueueItem = useCallback(
const retryQueueItem = useRetryQueueItem();
const onClickRetryQueueItem = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
retryQueueItem();
retryQueueItem.trigger(item.item_id);
},
[retryQueueItem]
[item.item_id, retryQueueItem]
);
const isOpen = useMemo(() => context.openQueueItems.includes(item.item_id), [context.openQueueItems, item.item_id]);
@@ -135,17 +135,17 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
<ButtonGroup size="xs" variant="ghost">
{(!isFailed || !isRetryEnabled || isValidationRun) && (
<IconButton
onClick={handleCancelQueueItem}
onClick={onClickDeleteQueueItem}
isDisabled={isCanceled}
isLoading={isLoadingCancelQueueItem}
isLoading={deleteQueueItem.isLoading}
aria-label={t('queue.cancelItem')}
icon={<PiXBold />}
/>
)}
{isFailed && isRetryEnabled && !isValidationRun && (
<IconButton
onClick={handleRetryQueueItem}
isLoading={isLoadingRetryQueueItem}
onClick={onClickRetryQueueItem}
isLoading={retryQueueItem.isLoading}
aria-label={t('queue.retryItem')}
icon={<PiArrowCounterClockwiseBold />}
/>

View File

@@ -2,14 +2,15 @@ import { Button, ButtonGroup, Flex, Heading, Spinner, Text } from '@invoke-ai/ui
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
import { useDestinationText } from 'features/queue/components/QueueList/useDestinationText';
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
import { useBatchIsCanceled } from 'features/queue/hooks/useBatchIsCanceled';
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem';
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { get } from 'lodash-es';
import type { ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiXBold } from 'react-icons/pi';
import type { S } from 'services/api/types';
@@ -22,9 +23,10 @@ const QueueItemComponent = ({ queueItem }: Props) => {
const { session_id, batch_id, item_id, origin, destination } = queueItem;
const { t } = useTranslation();
const isRetryEnabled = useFeatureStatus('retryQueueItem');
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled: isBatchCanceled } = useCancelBatch(batch_id);
const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item_id);
const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item_id);
const isBatchCanceled = useBatchIsCanceled(batch_id);
const cancelBatch = useCancelBatch();
const deleteQueueItem = useDeleteQueueItem();
const retryQueueItem = useRetryQueueItem();
const originText = useOriginText(origin);
const destinationText = useDestinationText(destination);
@@ -50,6 +52,18 @@ const QueueItemComponent = ({ queueItem }: Props) => {
const isFailed = useMemo(() => !!queueItem && ['canceled', 'failed'].includes(queueItem.status), [queueItem]);
const onCancelBatch = useCallback(() => {
cancelBatch.trigger(batch_id);
}, [cancelBatch, batch_id]);
const onCancelQueueItem = useCallback(() => {
deleteQueueItem.trigger(item_id);
}, [deleteQueueItem, item_id]);
const onRetryQueueItem = useCallback(() => {
retryQueueItem.trigger(item_id);
}, [retryQueueItem, item_id]);
return (
<Flex layerStyle="third" flexDir="column" p={2} pt={0} borderRadius="base" gap={2}>
<Flex
@@ -70,9 +84,9 @@ const QueueItemComponent = ({ queueItem }: Props) => {
<ButtonGroup size="xs" orientation="vertical">
{(!isFailed || !isRetryEnabled) && (
<Button
onClick={cancelQueueItem}
isLoading={isLoadingCancelQueueItem}
isDisabled={queueItem ? isCanceled : true}
onClick={onCancelQueueItem}
isLoading={deleteQueueItem.isLoading}
isDisabled={deleteQueueItem.isDisabled || queueItem ? isCanceled : true}
aria-label={t('queue.cancelItem')}
leftIcon={<PiXBold />}
colorScheme="error"
@@ -82,9 +96,9 @@ const QueueItemComponent = ({ queueItem }: Props) => {
)}
{isFailed && isRetryEnabled && (
<Button
onClick={retryQueueItem}
isLoading={isLoadingRetryQueueItem}
isDisabled={!queueItem}
onClick={onRetryQueueItem}
isLoading={retryQueueItem.isLoading}
isDisabled={retryQueueItem.isDisabled || !queueItem}
aria-label={t('queue.retryItem')}
leftIcon={<PiArrowCounterClockwiseBold />}
colorScheme="invokeBlue"
@@ -93,9 +107,9 @@ const QueueItemComponent = ({ queueItem }: Props) => {
</Button>
)}
<Button
onClick={cancelBatch}
isLoading={isLoadingCancelBatch}
isDisabled={isBatchCanceled}
onClick={onCancelBatch}
isLoading={cancelBatch.isLoading}
isDisabled={cancelBatch.isDisabled || isBatchCanceled}
aria-label={t('queue.cancelBatch')}
leftIcon={<PiXBold />}
colorScheme="error"

View File

@@ -1,6 +1,6 @@
/* eslint-disable i18next/no-literal-string */
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
import { CancelAllExceptCurrentButton } from 'features/queue/components/CancelAllExceptCurrentButton';
import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
@@ -24,7 +24,7 @@ const QueueTabQueueControls = () => {
)}
<ButtonGroup w={28} orientation="vertical" size="sm">
<PruneQueueButton />
<CancelAllExceptCurrentButton />
<DeleteAllExceptCurrentQueueItemConfirmationAlertDialog />
</ButtonGroup>
</Flex>
<ClearModelCacheButton />

View File

@@ -11,17 +11,17 @@ type Props = {
const ResumeProcessorButton = ({ asIconButton }: Props) => {
const { t } = useTranslation();
const { resumeProcessor, isLoading, isDisabled } = useResumeProcessor();
const resumeProcessor = useResumeProcessor();
return (
<QueueButton
asIconButton={asIconButton}
label={t('queue.resume')}
tooltip={t('queue.resumeTooltip')}
isDisabled={isDisabled}
isLoading={isLoading}
isDisabled={resumeProcessor.isDisabled}
isLoading={resumeProcessor.isLoading}
icon={<PiPlayFill />}
onClick={resumeProcessor}
onClick={resumeProcessor.trigger}
colorScheme="green"
/>
);

View File

@@ -9,28 +9,28 @@ const ToggleInvocationCacheButton = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
const {
enableInvocationCache,
isDisabled: isEnableDisabled,
isLoading: isEnableLoading,
} = useEnableInvocationCache();
const enableInvocationCache = useEnableInvocationCache();
const {
disableInvocationCache,
isDisabled: isDisableDisabled,
isLoading: isDisableLoading,
} = useDisableInvocationCache();
const disableInvocationCache = useDisableInvocationCache();
if (cacheStatus?.enabled) {
return (
<Button isDisabled={isDisableDisabled} isLoading={isDisableLoading} onClick={disableInvocationCache}>
<Button
onClick={disableInvocationCache.trigger}
isDisabled={disableInvocationCache.isDisabled}
isLoading={disableInvocationCache.isLoading}
>
{t('invocationCache.disable')}
</Button>
);
}
return (
<Button isDisabled={isEnableDisabled} isLoading={isEnableLoading} onClick={enableInvocationCache}>
<Button
onClick={enableInvocationCache.trigger}
isDisabled={enableInvocationCache.isDisabled}
isLoading={enableInvocationCache.isLoading}
>
{t('invocationCache.enable')}
</Button>
);

View File

@@ -0,0 +1,20 @@
import { useGetBatchStatusQuery } from 'services/api/endpoints/queue';
export const useBatchIsCanceled = (batch_id: string) => {
const { isCanceled } = useGetBatchStatusQuery(
{ batch_id },
{
selectFromResult: ({ data }) => {
if (!data) {
return { isCanceled: true };
}
return {
isCanceled: data?.in_progress === 0 && data?.pending === 0,
};
},
}
);
return isCanceled;
};

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -9,17 +9,17 @@ export const useCancelAllExceptCurrentQueueItem = () => {
const { t } = useTranslation();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useCancelAllExceptCurrentMutation({
const [_trigger, { isLoading }] = useCancelAllExceptCurrentMutation({
fixedCacheKey: 'cancelAllExceptCurrent',
});
const cancelAllExceptCurrentQueueItem = useCallback(async () => {
const trigger = useCallback(async () => {
if (!queueStatus?.queue.pending) {
return;
}
try {
await trigger().unwrap();
await _trigger().unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
@@ -32,17 +32,7 @@ export const useCancelAllExceptCurrentQueueItem = () => {
status: 'error',
});
}
}, [queueStatus?.queue.pending, trigger, t]);
}, [queueStatus?.queue.pending, _trigger, t]);
const isDisabled = useMemo(
() => !isConnected || !queueStatus?.queue.pending,
[isConnected, queueStatus?.queue.pending]
);
return {
cancelAllExceptCurrentQueueItem,
isLoading,
queueStatus,
isDisabled,
};
return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.pending } as const;
};

View File

@@ -2,48 +2,34 @@ import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelByBatchIdsMutation, useGetBatchStatusQuery } from 'services/api/endpoints/queue';
import { useCancelByBatchIdsMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useCancelBatch = (batch_id: string) => {
export const useCancelBatch = () => {
const isConnected = useStore($isConnected);
const { isCanceled } = useGetBatchStatusQuery(
{ batch_id },
{
selectFromResult: ({ data }) => {
if (!data) {
return { isCanceled: true };
}
return {
isCanceled: data?.in_progress === 0 && data?.pending === 0,
};
},
}
);
const [trigger, { isLoading }] = useCancelByBatchIdsMutation({
const [_trigger, { isLoading }] = useCancelByBatchIdsMutation({
fixedCacheKey: 'cancelByBatchIds',
});
const { t } = useTranslation();
const cancelBatch = useCallback(async () => {
if (isCanceled) {
return;
}
try {
await trigger({ batch_ids: [batch_id] }).unwrap();
toast({
id: 'CANCEL_BATCH_SUCCEEDED',
title: t('queue.cancelBatchSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'CANCEL_BATCH_FAILED',
title: t('queue.cancelBatchFailed'),
status: 'error',
});
}
}, [batch_id, isCanceled, t, trigger]);
const trigger = useCallback(
async (batch_id: string) => {
try {
await _trigger({ batch_ids: [batch_id] }).unwrap();
toast({
id: 'CANCEL_BATCH_SUCCEEDED',
title: t('queue.cancelBatchSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'CANCEL_BATCH_FAILED',
title: t('queue.cancelBatchFailed'),
status: 'error',
});
}
},
[t, _trigger]
);
return { cancelBatch, isLoading, isCanceled, isDisabled: !isConnected };
return { trigger, isLoading, isDisabled: !isConnected };
};

View File

@@ -1,43 +1,20 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { isNil } from 'lodash-es';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelQueueItemMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
import { useCurrentQueueItemId } from 'features/queue/hooks/useCurrentQueueItemId';
import { useCallback } from 'react';
export const useCancelCurrentQueueItem = () => {
const isConnected = useStore($isConnected);
const { data: queueStatus } = useGetQueueStatusQuery();
const [trigger, { isLoading }] = useCancelQueueItemMutation();
const { t } = useTranslation();
const currentQueueItemId = useMemo(() => queueStatus?.queue.item_id, [queueStatus?.queue.item_id]);
const cancelQueueItem = useCallback(async () => {
if (currentQueueItemId !== null || currentQueueItemId !== undefined) {
const currentQueueItemId = useCurrentQueueItemId();
const cancelQueueItem = useCancelQueueItem();
const trigger = useCallback(() => {
if (currentQueueItemId === null) {
return;
}
try {
await trigger({ item_id: currentQueueItemId }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_CANCEL_FAILED',
title: t('queue.cancelFailed'),
status: 'error',
});
}
}, [currentQueueItemId, t, trigger]);
const isDisabled = useMemo(() => !isConnected || isNil(currentQueueItemId), [isConnected, currentQueueItemId]);
cancelQueueItem.trigger(currentQueueItemId);
}, [currentQueueItemId, cancelQueueItem]);
return {
cancelQueueItem,
isLoading,
currentQueueItemId,
isDisabled,
trigger,
isLoading: cancelQueueItem.isLoading,
isDisabled: cancelQueueItem.isDisabled || currentQueueItemId === null,
};
};

View File

@@ -5,26 +5,29 @@ import { useTranslation } from 'react-i18next';
import { useCancelQueueItemMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useCancelQueueItem = (item_id: number) => {
export const useCancelQueueItem = () => {
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useCancelQueueItemMutation();
const [_trigger, { isLoading }] = useCancelQueueItemMutation();
const { t } = useTranslation();
const cancelQueueItem = useCallback(async () => {
try {
await trigger({ item_id }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_CANCEL_FAILED',
title: t('queue.cancelFailed'),
status: 'error',
});
}
}, [item_id, t, trigger]);
const trigger = useCallback(
async (item_id: number) => {
try {
await _trigger({ item_id }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_CANCEL_FAILED',
title: t('queue.cancelFailed'),
status: 'error',
});
}
},
[t, _trigger]
);
return { cancelQueueItem, isLoading, isDisabled: !isConnected };
return { trigger, isLoading, isDisabled: !isConnected };
};

View File

@@ -0,0 +1,33 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useCancelQueueItemsByDestination = () => {
const isConnected = useStore($isConnected);
const [_trigger, { isLoading }] = useCancelQueueItemsByDestinationMutation();
const { t } = useTranslation();
const trigger = useCallback(
async (destination: string) => {
try {
await _trigger({ destination }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_CANCEL_FAILED',
title: t('queue.cancelFailed'),
status: 'error',
});
}
},
[t, _trigger]
);
return { trigger, isLoading, isDisabled: !isConnected };
};

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
@@ -9,19 +9,13 @@ export const useClearInvocationCache = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useClearInvocationCacheMutation({
const [_trigger, { isLoading }] = useClearInvocationCacheMutation({
fixedCacheKey: 'clearInvocationCache',
});
const isDisabled = useMemo(() => !cacheStatus?.size || !isConnected, [cacheStatus?.size, isConnected]);
const clearInvocationCache = useCallback(async () => {
if (isDisabled) {
return;
}
const trigger = useCallback(async () => {
try {
await trigger().unwrap();
await _trigger().unwrap();
toast({
id: 'INVOCATION_CACHE_CLEAR_SUCCEEDED',
title: t('invocationCache.clearSucceeded'),
@@ -34,7 +28,7 @@ export const useClearInvocationCache = () => {
status: 'error',
});
}
}, [isDisabled, trigger, t]);
}, [_trigger, t]);
return { clearInvocationCache, isLoading, cacheStatus, isDisabled };
return { trigger, isLoading, isDisabled: !isConnected || !cacheStatus?.size };
};

View File

@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -12,17 +12,17 @@ export const useClearQueue = () => {
const dispatch = useAppDispatch();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useClearQueueMutation({
const [_trigger, { isLoading }] = useClearQueueMutation({
fixedCacheKey: 'clearQueue',
});
const clearQueue = useCallback(async () => {
const trigger = useCallback(async () => {
if (!queueStatus?.queue.total) {
return;
}
try {
await trigger().unwrap();
await _trigger().unwrap();
toast({
id: 'QUEUE_CLEAR_SUCCEEDED',
title: t('queue.clearSucceeded'),
@@ -37,14 +37,7 @@ export const useClearQueue = () => {
status: 'error',
});
}
}, [queueStatus?.queue.total, trigger, dispatch, t]);
}, [queueStatus?.queue.total, _trigger, dispatch, t]);
const isDisabled = useMemo(() => !isConnected || !queueStatus?.queue.total, [isConnected, queueStatus?.queue.total]);
return {
clearQueue,
isLoading,
queueStatus,
isDisabled,
};
return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.total };
};

View File

@@ -1,11 +0,0 @@
import { useGetCurrentQueueItemQuery } from 'services/api/endpoints/queue';
export const useCurrentDestination = () => {
const { destination } = useGetCurrentQueueItemQuery(undefined, {
selectFromResult: ({ data }) => ({
destination: data ? data.destination : null,
}),
});
return destination;
};

View File

@@ -0,0 +1,11 @@
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
export const useCurrentQueueItemId = () => {
const { currentQueueItemId } = useGetQueueStatusQuery(undefined, {
selectFromResult: ({ data }) => ({
currentQueueItemId: data?.queue.item_id ?? null,
}),
});
return currentQueueItemId;
};

View File

@@ -0,0 +1,38 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDeleteAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useDeleteAllExceptCurrentQueueItem = () => {
const { t } = useTranslation();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
const [_trigger, { isLoading }] = useDeleteAllExceptCurrentMutation({
fixedCacheKey: 'deleteAllExceptCurrent',
});
const trigger = useCallback(async () => {
if (!queueStatus?.queue.pending) {
return;
}
try {
await _trigger().unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_CANCEL_FAILED',
title: t('queue.cancelFailed'),
status: 'error',
});
}
}, [queueStatus?.queue.pending, _trigger, t]);
return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.queue.pending } as const;
};

View File

@@ -0,0 +1,20 @@
import { useCurrentQueueItemId } from 'features/queue/hooks/useCurrentQueueItemId';
import { useDeleteQueueItem } from 'features/queue/hooks/useDeleteQueueItem';
import { useCallback } from 'react';
export const useDeleteCurrentQueueItem = () => {
const currentQueueItemId = useCurrentQueueItemId();
const deleteQueueItem = useDeleteQueueItem();
const trigger = useCallback(() => {
if (currentQueueItemId === null) {
return;
}
deleteQueueItem.trigger(currentQueueItemId);
}, [currentQueueItemId, deleteQueueItem]);
return {
trigger,
isLoading: deleteQueueItem.isLoading,
isDisabled: deleteQueueItem.isDisabled || currentQueueItemId === null,
};
};

View File

@@ -0,0 +1,33 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDeleteQueueItemMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useDeleteQueueItem = () => {
const isConnected = useStore($isConnected);
const [_trigger, { isLoading }] = useDeleteQueueItemMutation();
const { t } = useTranslation();
const trigger = useCallback(
async (item_id: number) => {
try {
await _trigger({ item_id }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_CANCEL_FAILED',
title: t('queue.cancelFailed'),
status: 'error',
});
}
},
[t, _trigger]
);
return { trigger, isLoading, isDisabled: !isConnected };
};

View File

@@ -0,0 +1,33 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDeleteQueueItemsByDestinationMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useDeleteQueueItemsByDestination = () => {
const isConnected = useStore($isConnected);
const [_trigger, { isLoading }] = useDeleteQueueItemsByDestinationMutation();
const { t } = useTranslation();
const trigger = useCallback(
async (destination: string) => {
try {
await _trigger({ destination }).unwrap();
toast({
id: 'QUEUE_CANCEL_SUCCEEDED',
title: t('queue.cancelSucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_CANCEL_FAILED',
title: t('queue.cancelFailed'),
status: 'error',
});
}
},
[t, _trigger]
);
return { trigger, isLoading, isDisabled: !isConnected };
};

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDisableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
@@ -9,22 +9,13 @@ export const useDisableInvocationCache = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useDisableInvocationCacheMutation({
const [_trigger, { isLoading }] = useDisableInvocationCacheMutation({
fixedCacheKey: 'disableInvocationCache',
});
const isDisabled = useMemo(
() => !cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
[cacheStatus?.enabled, cacheStatus?.max_size, isConnected]
);
const disableInvocationCache = useCallback(async () => {
if (isDisabled) {
return;
}
const trigger = useCallback(async () => {
try {
await trigger().unwrap();
await _trigger().unwrap();
toast({
id: 'INVOCATION_CACHE_DISABLE_SUCCEEDED',
title: t('invocationCache.disableSucceeded'),
@@ -37,7 +28,12 @@ export const useDisableInvocationCache = () => {
status: 'error',
});
}
}, [isDisabled, trigger, t]);
}, [_trigger, t]);
return { disableInvocationCache, isLoading, cacheStatus, isDisabled };
return {
trigger,
isLoading,
cacheStatus,
isDisabled: !cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
};
};

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useEnableInvocationCacheMutation, useGetInvocationCacheStatusQuery } from 'services/api/endpoints/appInfo';
import { $isConnected } from 'services/events/stores';
@@ -9,22 +9,13 @@ export const useEnableInvocationCache = () => {
const { t } = useTranslation();
const { data: cacheStatus } = useGetInvocationCacheStatusQuery();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useEnableInvocationCacheMutation({
const [_trigger, { isLoading }] = useEnableInvocationCacheMutation({
fixedCacheKey: 'enableInvocationCache',
});
const isDisabled = useMemo(
() => cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
[cacheStatus?.enabled, cacheStatus?.max_size, isConnected]
);
const enableInvocationCache = useCallback(async () => {
if (isDisabled) {
return;
}
const trigger = useCallback(async () => {
try {
await trigger().unwrap();
await _trigger().unwrap();
toast({
id: 'INVOCATION_CACHE_ENABLE_SUCCEEDED',
title: t('invocationCache.enableSucceeded'),
@@ -37,7 +28,11 @@ export const useEnableInvocationCache = () => {
status: 'error',
});
}
}, [isDisabled, trigger, t]);
}, [_trigger, t]);
return { enableInvocationCache, isLoading, cacheStatus, isDisabled };
return {
trigger,
isLoading,
isDisabled: cacheStatus?.enabled || !isConnected || cacheStatus?.max_size === 0,
};
};

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, usePauseProcessorMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -9,18 +9,13 @@ export const usePauseProcessor = () => {
const { t } = useTranslation();
const isConnected = useStore($isConnected);
const { data: queueStatus } = useGetQueueStatusQuery();
const [trigger, { isLoading }] = usePauseProcessorMutation({
const [_trigger, { isLoading }] = usePauseProcessorMutation({
fixedCacheKey: 'pauseProcessor',
});
const isStarted = useMemo(() => Boolean(queueStatus?.processor.is_started), [queueStatus?.processor.is_started]);
const pauseProcessor = useCallback(async () => {
if (!isStarted) {
return;
}
const trigger = useCallback(async () => {
try {
await trigger().unwrap();
await _trigger().unwrap();
toast({
id: 'PAUSE_SUCCEEDED',
title: t('queue.pauseSucceeded'),
@@ -33,9 +28,7 @@ export const usePauseProcessor = () => {
status: 'error',
});
}
}, [isStarted, trigger, t]);
}, [_trigger, t]);
const isDisabled = useMemo(() => !isConnected || !isStarted, [isConnected, isStarted]);
return { pauseProcessor, isLoading, isStarted, isDisabled };
return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started };
};

View File

@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
import { useAppDispatch } from 'app/store/storeHooks';
import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, usePruneQueueMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -11,27 +11,14 @@ export const usePruneQueue = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = usePruneQueueMutation({
const finishedCount = useFinishedCount();
const [_trigger, { isLoading }] = usePruneQueueMutation({
fixedCacheKey: 'pruneQueue',
});
const { finishedCount } = useGetQueueStatusQuery(undefined, {
selectFromResult: ({ data }) => {
if (!data) {
return { finishedCount: 0 };
}
return {
finishedCount: data.queue.completed + data.queue.canceled + data.queue.failed,
};
},
});
const pruneQueue = useCallback(async () => {
if (!finishedCount) {
return;
}
const trigger = useCallback(async () => {
try {
const data = await trigger().unwrap();
const data = await _trigger().unwrap();
toast({
id: 'PRUNE_SUCCEEDED',
title: t('queue.pruneSucceeded', { item_count: data.deleted }),
@@ -46,9 +33,23 @@ export const usePruneQueue = () => {
status: 'error',
});
}
}, [finishedCount, trigger, dispatch, t]);
}, [_trigger, dispatch, t]);
const isDisabled = useMemo(() => !isConnected || !finishedCount, [finishedCount, isConnected]);
return { pruneQueue, isLoading, finishedCount, isDisabled };
return { trigger, isLoading, isDisabled: !isConnected || !finishedCount };
};
export const useFinishedCount = () => {
const { finishedCount } = useGetQueueStatusQuery(undefined, {
selectFromResult: ({ data }) => {
if (!data) {
return { finishedCount: 0 };
}
return {
finishedCount: data.queue.completed + data.queue.canceled + data.queue.failed,
};
},
});
return finishedCount;
};

View File

@@ -1,6 +1,6 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery, useResumeProcessorMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
@@ -9,18 +9,13 @@ export const useResumeProcessor = () => {
const isConnected = useStore($isConnected);
const { data: queueStatus } = useGetQueueStatusQuery();
const { t } = useTranslation();
const [trigger, { isLoading }] = useResumeProcessorMutation({
const [_trigger, { isLoading }] = useResumeProcessorMutation({
fixedCacheKey: 'resumeProcessor',
});
const isStarted = useMemo(() => Boolean(queueStatus?.processor.is_started), [queueStatus?.processor.is_started]);
const resumeProcessor = useCallback(async () => {
if (isStarted) {
return;
}
const trigger = useCallback(async () => {
try {
await trigger().unwrap();
await _trigger().unwrap();
toast({
id: 'PROCESSOR_RESUMED',
title: t('queue.resumeSucceeded'),
@@ -33,9 +28,7 @@ export const useResumeProcessor = () => {
status: 'error',
});
}
}, [isStarted, trigger, t]);
}, [_trigger, t]);
const isDisabled = useMemo(() => !isConnected || isStarted, [isConnected, isStarted]);
return { resumeProcessor, isLoading, isStarted, isDisabled };
return { trigger, isLoading, isDisabled: !isConnected || !queueStatus?.processor.is_started };
};

View File

@@ -5,29 +5,32 @@ import { useTranslation } from 'react-i18next';
import { useRetryItemsByIdMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useRetryQueueItem = (item_id: number) => {
export const useRetryQueueItem = () => {
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useRetryItemsByIdMutation();
const [_trigger, { isLoading }] = useRetryItemsByIdMutation();
const { t } = useTranslation();
const retryQueueItem = useCallback(async () => {
try {
const result = await trigger([item_id]).unwrap();
if (!result.retried_item_ids.includes(item_id)) {
throw new Error('Failed to retry item');
const trigger = useCallback(
async (item_id: number) => {
try {
const result = await _trigger([item_id]).unwrap();
if (!result.retried_item_ids.includes(item_id)) {
throw new Error('Failed to retry item');
}
toast({
id: 'QUEUE_RETRY_SUCCEEDED',
title: t('queue.retrySucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_RETRY_FAILED',
title: t('queue.retryFailed'),
status: 'error',
});
}
toast({
id: 'QUEUE_RETRY_SUCCEEDED',
title: t('queue.retrySucceeded'),
status: 'success',
});
} catch {
toast({
id: 'QUEUE_RETRY_FAILED',
title: t('queue.retryFailed'),
status: 'error',
});
}
}, [item_id, t, trigger]);
},
[t, _trigger]
);
return { retryQueueItem, isLoading, isDisabled: !isConnected };
return { trigger, isLoading, isDisabled: !isConnected };
};

View File

@@ -1,6 +1,5 @@
import { Progress } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
@@ -8,7 +7,6 @@ import { $isConnected, $lastProgressEvent } from 'services/events/stores';
const ProgressBar = () => {
const { t } = useTranslation();
const destination = useCurrentDestination();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
const lastProgressEvent = useStore($lastProgressEvent);
@@ -39,16 +37,6 @@ const ProgressBar = () => {
return false;
}, [isConnected, lastProgressEvent, queueStatus?.queue.in_progress]);
const colorScheme = useMemo(() => {
if (destination === 'canvas') {
return 'invokeGreen';
} else if (destination === 'gallery') {
return 'invokeBlue';
} else {
return 'base';
}
}, [destination]);
return (
<Progress
value={value}
@@ -56,7 +44,7 @@ const ProgressBar = () => {
isIndeterminate={isIndeterminate}
h={2}
w="full"
colorScheme={colorScheme}
colorScheme="invokeBlue"
/>
);
};

View File

@@ -3,9 +3,9 @@ import { useAppSelector } from 'app/store/storeHooks';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { useCancelAllExceptCurrentQueueItemDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
import { useInvoke } from 'features/queue/hooks/useInvoke';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
@@ -34,8 +34,8 @@ export const FloatingLeftPanelButtons = memo((props: { onToggle: () => void }) =
<ButtonGroup orientation="vertical" h={48}>
<ToggleLeftPanelButton onToggle={props.onToggle} />
<InvokeIconButton />
<CancelCurrentIconButton />
<CancelAllExceptCurrentIconButton />
<DeleteCurrentIconButton />
<DeleteAllExceptCurrentIconButton />
</ButtonGroup>
</Flex>
);
@@ -103,18 +103,18 @@ const InvokeIconButtonIcon = memo(() => {
});
InvokeIconButtonIcon.displayName = 'InvokeIconButtonIcon';
const CancelCurrentIconButton = memo(() => {
const DeleteCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelCurrentQueueItem = useCancelCurrentQueueItem();
const deleteCurrentQueueItem = useDeleteCurrentQueueItem();
return (
<Tooltip label={t('queue.cancelTooltip')} placement="end">
<IconButton
isDisabled={cancelCurrentQueueItem.isDisabled}
isLoading={cancelCurrentQueueItem.isLoading}
onClick={deleteCurrentQueueItem.trigger}
isDisabled={deleteCurrentQueueItem.isDisabled}
isLoading={deleteCurrentQueueItem.isLoading}
aria-label={t('queue.cancelTooltip')}
icon={<PiXBold />}
onClick={cancelCurrentQueueItem.cancelQueueItem}
colorScheme="error"
flexGrow={1}
/>
@@ -122,25 +122,25 @@ const CancelCurrentIconButton = memo(() => {
);
});
CancelCurrentIconButton.displayName = 'CancelCurrentIconButton';
DeleteCurrentIconButton.displayName = 'DeleteCurrentIconButton';
const CancelAllExceptCurrentIconButton = memo(() => {
const DeleteAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
return (
<Tooltip label={t('queue.cancelAllExceptCurrentTooltip')} placement="end">
<IconButton
isDisabled={cancelAllExceptCurrent.isDisabled}
isLoading={cancelAllExceptCurrent.isLoading}
isDisabled={deleteAllExceptCurrent.isDisabled}
isLoading={deleteAllExceptCurrent.isLoading}
aria-label={t('queue.clear')}
icon={<PiXCircle />}
colorScheme="error"
onClick={cancelAllExceptCurrent.openDialog}
onClick={deleteAllExceptCurrent.openDialog}
flexGrow={1}
/>
</Tooltip>
);
});
CancelAllExceptCurrentIconButton.displayName = 'CancelAllExceptCurrentIconButton';
DeleteAllExceptCurrentIconButton.displayName = 'DeleteAllExceptCurrentIconButton';

View File

@@ -224,7 +224,7 @@ export const queueApi = api.injectEndpoints({
];
},
}),
cancelByDestination: build.mutation<
cancelQueueItemsByDestination: build.mutation<
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/cancel_by_destination']['put']['parameters']['query']
>({
@@ -256,6 +256,16 @@ export const queueApi = api.injectEndpoints({
}),
invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination', 'SessionQueueItem'],
}),
deleteAllExceptCurrent: build.mutation<
paths['/api/v1/queue/{queue_id}/delete_all_except_current']['put']['responses']['200']['content']['application/json'],
void
>({
query: () => ({
url: buildQueueUrl('delete_all_except_current'),
method: 'PUT',
}),
invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination', 'SessionQueueItem'],
}),
retryItemsById: build.mutation<
paths['/api/v1/queue/{queue_id}/retry_items_by_id']['put']['responses']['200']['content']['application/json'],
paths['/api/v1/queue/{queue_id}/retry_items_by_id']['put']['requestBody']['content']['application/json']
@@ -329,7 +339,11 @@ export const queueApi = api.injectEndpoints({
url: buildQueueUrl(`i/${item_id}`),
method: 'DELETE',
}),
invalidatesTags: (result, error, { item_id }) => [{ type: 'SessionQueueItem', id: item_id }],
invalidatesTags: (result, error, { item_id }) => [
{ type: 'SessionQueueItem', id: item_id },
{ type: 'SessionQueueItem', id: LIST_TAG },
{ type: 'SessionQueueItem', id: LIST_ALL_TAG },
],
}),
deleteQueueItemsByDestination: build.mutation<void, { destination: string }>({
query: ({ destination }) => ({
@@ -366,8 +380,10 @@ export const {
useGetQueueStatusQuery,
useListQueueItemsQuery,
useCancelQueueItemMutation,
useCancelQueueItemsByDestinationMutation,
useDeleteQueueItemMutation,
useDeleteQueueItemsByDestinationMutation,
useDeleteAllExceptCurrentMutation,
useGetBatchStatusQuery,
useGetCurrentQueueItemQuery,
useGetQueueCountsByDestinationQuery,

View File

@@ -1244,6 +1244,26 @@ export type paths = {
patch?: never;
trace?: never;
};
"/api/v1/queue/{queue_id}/delete_all_except_current": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/**
* Delete All Except Current
* @description Immediately deletes all queue items except in-processing items
*/
put: operations["delete_all_except_current"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/queue/{queue_id}/cancel_by_batch_ids": {
parameters: {
query?: never;
@@ -5885,6 +5905,17 @@ export type components = {
*/
type: "dw_openpose_detection";
};
/**
* DeleteAllExceptCurrentResult
* @description Result of deleting all except current
*/
DeleteAllExceptCurrentResult: {
/**
* Deleted
* @description Number of queue items deleted
*/
deleted: number;
};
/** DeleteBoardResult */
DeleteBoardResult: {
/**
@@ -24570,6 +24601,38 @@ export interface operations {
};
};
};
delete_all_except_current: {
parameters: {
query?: never;
header?: never;
path: {
/** @description The queue id to perform this operation on */
queue_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DeleteAllExceptCurrentResult"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
cancel_by_batch_ids: {
parameters: {
query?: never;