feat(ui): add retry buttons to queue tab

- Add the new HTTP endpoint to the queue client
- Add buttons to the queue items to retry them
This commit is contained in:
psychedelicious
2025-02-17 15:24:54 +10:00
parent 926f69677a
commit 7ee636b68b
7 changed files with 127 additions and 23 deletions

View File

@@ -4,11 +4,12 @@ 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 { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem';
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { PiArrowCounterClockwiseBold, PiXBold } from 'react-icons/pi';
import type { SessionQueueItemDTO } from 'services/api/types';
import { COLUMN_WIDTHS } from './constants';
@@ -33,7 +34,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
const handleToggle = useCallback(() => {
context.toggleQueueItem(item.item_id);
}, [context, item.item_id]);
const { cancelQueueItem, isLoading } = useCancelQueueItem(item.item_id);
const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item.item_id);
const handleCancelQueueItem = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
@@ -41,6 +42,14 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
},
[cancelQueueItem]
);
const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item.item_id);
const handleRetryQueueItem = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
retryQueueItem();
},
[retryQueueItem]
);
const isOpen = useMemo(() => context.openQueueItems.includes(item.item_id), [context.openQueueItems, item.item_id]);
const executionTime = useMemo(() => {
@@ -52,10 +61,10 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
}, [item]);
const isCanceled = useMemo(() => ['canceled', 'completed', 'failed'].includes(item.status), [item.status]);
const isFailed = useMemo(() => ['canceled', 'failed'].includes(item.status), [item.status]);
const originText = useOriginText(item.origin);
const destinationText = useDestinationText(item.destination);
const icon = useMemo(() => <PiXBold />, []);
return (
<Flex
flexDir="column"
@@ -109,13 +118,23 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
</Flex>
<Flex alignItems="center" w={COLUMN_WIDTHS.actions} pe={3}>
<ButtonGroup size="xs" variant="ghost">
<IconButton
onClick={handleCancelQueueItem}
isDisabled={isCanceled}
isLoading={isLoading}
aria-label={t('queue.cancelItem')}
icon={icon}
/>
{!isFailed && (
<IconButton
onClick={handleCancelQueueItem}
isDisabled={isCanceled}
isLoading={isLoadingCancelQueueItem}
aria-label={t('queue.cancelItem')}
icon={<PiXBold />}
/>
)}
{isFailed && (
<IconButton
onClick={handleRetryQueueItem}
isLoading={isLoadingRetryQueueItem}
aria-label={t('queue.retryItem')}
icon={<PiArrowCounterClockwiseBold />}
/>
)}
</ButtonGroup>
</Flex>
</Flex>

View File

@@ -4,12 +4,13 @@ import { useDestinationText } from 'features/queue/components/QueueList/useDesti
import { useOriginText } from 'features/queue/components/QueueList/useOriginText';
import { useCancelBatch } from 'features/queue/hooks/useCancelBatch';
import { useCancelQueueItem } from 'features/queue/hooks/useCancelQueueItem';
import { useRetryQueueItem } from 'features/queue/hooks/useRetryQueueItem';
import { getSecondsFromTimestamps } from 'features/queue/util/getSecondsFromTimestamps';
import { get } from 'lodash-es';
import type { ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { PiArrowCounterClockwiseBold, PiXBold } from 'react-icons/pi';
import { useGetQueueItemQuery } from 'services/api/endpoints/queue';
import type { SessionQueueItemDTO } from 'services/api/types';
@@ -20,9 +21,10 @@ type Props = {
const QueueItemComponent = ({ queueItemDTO }: Props) => {
const { session_id, batch_id, item_id, origin, destination } = queueItemDTO;
const { t } = useTranslation();
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled } = useCancelBatch(batch_id);
const { cancelBatch, isLoading: isLoadingCancelBatch, isCanceled: isBatchCanceled } = useCancelBatch(batch_id);
const { cancelQueueItem, isLoading: isLoadingCancelQueueItem } = useCancelQueueItem(item_id);
const { retryQueueItem, isLoading: isLoadingRetryQueueItem } = useRetryQueueItem(item_id);
const { data: queueItem } = useGetQueueItemQuery(item_id);
@@ -43,6 +45,13 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
return `${seconds}s`;
}, [queueItem, t]);
const isCanceled = useMemo(
() => !!queueItem && ['canceled', 'completed', 'failed'].includes(queueItem.status),
[queueItem]
);
const isFailed = useMemo(() => !!queueItem && ['canceled', 'failed'].includes(queueItem.status), [queueItem]);
return (
<Flex layerStyle="third" flexDir="column" p={2} pt={0} borderRadius="base" gap={2}>
<Flex
@@ -61,20 +70,34 @@ const QueueItemComponent = ({ queueItemDTO }: Props) => {
<QueueItemData label={t('queue.batch')} data={batch_id} />
<QueueItemData label={t('queue.session')} data={session_id} />
<ButtonGroup size="xs" orientation="vertical">
<Button
onClick={cancelQueueItem}
isLoading={isLoadingCancelQueueItem}
isDisabled={queueItem ? ['canceled', 'completed', 'failed'].includes(queueItem.status) : true}
aria-label={t('queue.cancelItem')}
leftIcon={<PiXBold />}
colorScheme="error"
>
{t('queue.cancelItem')}
</Button>
{!isFailed && (
<Button
onClick={cancelQueueItem}
isLoading={isLoadingCancelQueueItem}
isDisabled={queueItem ? isCanceled : true}
aria-label={t('queue.cancelItem')}
leftIcon={<PiXBold />}
colorScheme="error"
>
{t('queue.cancelItem')}
</Button>
)}
{isFailed && (
<Button
onClick={retryQueueItem}
isLoading={isLoadingRetryQueueItem}
isDisabled={!queueItem}
aria-label={t('queue.retryItem')}
leftIcon={<PiArrowCounterClockwiseBold />}
colorScheme="invokeBlue"
>
{t('queue.retryItem')}
</Button>
)}
<Button
onClick={cancelBatch}
isLoading={isLoadingCancelBatch}
isDisabled={isCanceled}
isDisabled={isBatchCanceled}
aria-label={t('queue.cancelBatch')}
leftIcon={<PiXBold />}
colorScheme="error"

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 { useRetryItemsByIdMutation } from 'services/api/endpoints/queue';
import { $isConnected } from 'services/events/stores';
export const useRetryQueueItem = (item_id: number) => {
const isConnected = useStore($isConnected);
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');
}
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]);
return { retryQueueItem, isLoading, isDisabled: !isConnected };
};