Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add cancel all except current queue item functionality #7395

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions invokeai/app/api/routers/session_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
CancelAllExceptCurrentResult,
CancelByBatchIDsResult,
CancelByDestinationResult,
ClearResult,
Expand Down Expand Up @@ -94,6 +95,18 @@ async def Pause(
return ApiDependencies.invoker.services.session_processor.pause()


@session_queue_router.put(
"/{queue_id}/cancel_all_except_current",
operation_id="cancel_all_except_current",
responses={200: {"model": CancelAllExceptCurrentResult}},
)
async def cancel_all_except_current(
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> CancelAllExceptCurrentResult:
"""Immediately cancels all queue items except in-processing items"""
return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id)


@session_queue_router.put(
"/{queue_id}/cancel_by_batch_ids",
operation_id="cancel_by_batch_ids",
Expand Down
6 changes: 6 additions & 0 deletions invokeai/app/services/session_queue/session_queue_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
CancelAllExceptCurrentResult,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByQueueIDResult,
Expand Down Expand Up @@ -112,6 +113,11 @@ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
"""Cancels all queue items with matching queue ID"""
pass

@abstractmethod
def cancel_all_except_current(self, queue_id: str) -> CancelAllExceptCurrentResult:
"""Cancels all queue items except in-progress items"""
pass

@abstractmethod
def list_queue_items(
self,
Expand Down
6 changes: 6 additions & 0 deletions invokeai/app/services/session_queue/session_queue_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,12 @@ class CancelByQueueIDResult(CancelByBatchIDsResult):
pass


class CancelAllExceptCurrentResult(CancelByBatchIDsResult):
"""Result of canceling all except current"""

pass


class IsEmptyResult(BaseModel):
"""Result of checking if the session queue is empty"""

Expand Down
34 changes: 34 additions & 0 deletions invokeai/app/services/session_queue/session_queue_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
CancelAllExceptCurrentResult,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByQueueIDResult,
Expand Down Expand Up @@ -510,6 +511,39 @@ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
self.__lock.release()
return CancelByQueueIDResult(canceled=count)

def cancel_all_except_current(self, queue_id: str) -> CancelAllExceptCurrentResult:
try:
where = """--sql
WHERE
queue_id == ?
AND status == 'pending'
"""
self.__lock.acquire()
self.__cursor.execute(
f"""--sql
SELECT COUNT(*)
FROM session_queue
{where};
""",
(queue_id,),
)
count = self.__cursor.fetchone()[0]
self.__cursor.execute(
f"""--sql
UPDATE session_queue
SET status = 'canceled'
{where};
""",
(queue_id,),
)
self.__conn.commit()
except Exception:
self.__conn.rollback()
raise
finally:
self.__lock.release()
return CancelAllExceptCurrentResult(canceled=count)

def get_queue_item(self, item_id: int) -> SessionQueueItem:
try:
self.__lock.acquire()
Expand Down
4 changes: 4 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,13 @@
"pauseSucceeded": "Processor Paused",
"pauseFailed": "Problem Pausing Processor",
"cancel": "Cancel",
"cancelAllExceptCurrentQueueItemAlertDialog": "Canceling all queue items except the current one will stop pending items but allow the in-progress one to finish.",
"cancelAllExceptCurrentQueueItemAlertDialog2": "Are you sure you want to cancel all pending queue items?",
"cancelAllExceptCurrentTooltip": "Cancel All Except Current Item",
"cancelTooltip": "Cancel Current Item",
"cancelSucceeded": "Item Canceled",
"cancelFailed": "Problem Canceling Item",
"confirm": "Confirm",
"prune": "Prune",
"pruneTooltip": "Prune {{item_count}} Completed Items",
"pruneSucceeded": "Pruned {{item_count}} Completed Items from Queue",
Expand Down
2 changes: 2 additions & 0 deletions invokeai/frontend/web/src/app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModa
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal';
import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { DeleteStylePresetDialog } from 'features/stylePresets/components/DeleteStylePresetDialog';
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
Expand Down Expand Up @@ -97,6 +98,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
<ChangeBoardModal />
<DynamicPromptsModal />
<StylePresetModal />
<CancelAllExceptCurrentQueueItemConfirmationAlertDialog />
<ClearQueueConfirmationsAlertDialog />
<NewWorkflowConfirmationAlertDialog />
<DeleteStylePresetDialog />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ConfirmationAlertDialog, Text } from '@invoke-ai/ui-library';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { useCancelAllExceptCurrentQueueItem } from 'features/queue/hooks/useCancelAllExceptCurrentQueueItem';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

const [useCancelAllExceptCurrentQueueItemConfirmationAlertDialog] = buildUseBoolean(false);

export const useCancelAllExceptCurrentQueueItemDialog = () => {
const dialog = useCancelAllExceptCurrentQueueItemConfirmationAlertDialog();
const { cancelAllExceptCurrentQueueItem, isLoading, isDisabled, queueStatus } = useCancelAllExceptCurrentQueueItem();

return {
cancelAllExceptCurrentQueueItem,
isOpen: dialog.isTrue,
openDialog: dialog.setTrue,
closeDialog: dialog.setFalse,
isLoading,
queueStatus,
isDisabled,
};
};

export const CancelAllExceptCurrentQueueItemConfirmationAlertDialog = memo(() => {
useAssertSingleton('CancelAllExceptCurrentQueueItemConfirmationAlertDialog');
const { t } = useTranslation();
const cancelAllExceptCurrentQueueItem = useCancelAllExceptCurrentQueueItemDialog();

return (
<ConfirmationAlertDialog
isOpen={cancelAllExceptCurrentQueueItem.isOpen}
onClose={cancelAllExceptCurrentQueueItem.closeDialog}
title={t('queue.cancelAllExceptCurrentTooltip')}
acceptCallback={cancelAllExceptCurrentQueueItem.cancelAllExceptCurrentQueueItem}
acceptButtonText={t('queue.confirm')}
useInert={false}
>
<Text>{t('queue.cancelAllExceptCurrentQueueItemAlertDialog')}</Text>
<br />
<Text>{t('queue.cancelAllExceptCurrentQueueItemAlertDialog2')}</Text>
</ConfirmationAlertDialog>
);
});

CancelAllExceptCurrentQueueItemConfirmationAlertDialog.displayName =
'CancelAllExceptCurrentQueueItemConfirmationAlertDialog';
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 { useClearQueueDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
Expand All @@ -10,14 +11,23 @@ import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiTrashSimpleBold, PiXBold } from 'react-icons/pi';
import {
PiListBold,
PiPauseFill,
PiPlayFill,
PiQueueBold,
PiTrashSimpleBold,
PiXBold,
PiXCircle,
} from 'react-icons/pi';

export const QueueActionsMenuButton = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const isPauseEnabled = useFeatureStatus('pauseQueue');
const isResumeEnabled = useFeatureStatus('resumeQueue');
const cancelAllExceptCurrent = useCancelAllExceptCurrentQueueItemDialog();
const cancelCurrent = useCancelCurrentQueueItem();
const clearQueue = useClearQueueDialog();
const {
Expand Down Expand Up @@ -52,6 +62,15 @@ export const QueueActionsMenuButton = memo(() => {
>
{t('queue.cancelTooltip')}
</MenuItem>
<MenuItem
isDestructive
icon={<PiXCircle />}
onClick={cancelAllExceptCurrent.openDialog}
isLoading={cancelAllExceptCurrent.isLoading}
isDisabled={cancelAllExceptCurrent.isDisabled}
>
{t('queue.cancelAllExceptCurrentTooltip')}
</MenuItem>
<MenuItem
isDestructive
icon={<PiTrashSimpleBold />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useStore } from '@nanostores/react';
import { toast } from 'features/toast/toast';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCancelAllExceptCurrentMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';

export const useCancelAllExceptCurrentQueueItem = () => {
const { t } = useTranslation();
const { data: queueStatus } = useGetQueueStatusQuery();
const isConnected = useStore($isConnected);
const [trigger, { isLoading }] = useCancelAllExceptCurrentMutation({
fixedCacheKey: 'cancelAllExceptCurrent',
});

const cancelAllExceptCurrentQueueItem = 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]);

const isDisabled = useMemo(
() => !isConnected || !queueStatus?.queue.pending,
[isConnected, queueStatus?.queue.pending]
);

return {
cancelAllExceptCurrentQueueItem,
isLoading,
queueStatus,
isDisabled,
};
};
20 changes: 20 additions & 0 deletions invokeai/frontend/web/src/services/api/endpoints/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,25 @@ export const queueApi = api.injectEndpoints({
return ['SessionQueueStatus', 'BatchStatus', { type: 'QueueCountsByDestination', id: destination }];
},
}),
cancelAllExceptCurrent: build.mutation<
paths['/api/v1/queue/{queue_id}/cancel_all_except_current']['put']['responses']['200']['content']['application/json'],
void
>({
query: () => ({
url: buildQueueUrl('cancel_all_except_current'),
method: 'PUT',
}),
onQueryStarted: async (arg, api) => {
const { dispatch, queryFulfilled } = api;
try {
await queryFulfilled;
resetListQueryData(dispatch);
} catch {
// no-op
}
},
invalidatesTags: ['SessionQueueStatus', 'BatchStatus', 'QueueCountsByDestination'],
}),
listQueueItems: build.query<
EntityState<components['schemas']['SessionQueueItemDTO'], string> & {
has_more: boolean;
Expand Down Expand Up @@ -390,6 +409,7 @@ export const queueApi = api.injectEndpoints({
});

export const {
useCancelAllExceptCurrentMutation,
useCancelByBatchIdsMutation,
useEnqueueBatchMutation,
usePauseProcessorMutation,
Expand Down
Loading
Loading