From 2b61addb519f479b79e0cab1bf5fd4e921c97052 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Tue, 24 Feb 2026 01:20:46 -0700 Subject: [PATCH] feat(model manager): redesign queue --- .../ModelInstallQueue/ModelInstallQueue.tsx | 121 +++++--- .../ModelInstallQueueBadge.tsx | 2 +- .../ModelInstallQueueItem.tsx | 261 ++++++++++++------ .../subpanels/InstallModels.tsx | 5 +- 4 files changed, 266 insertions(+), 123 deletions(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx index 6050ab84b2..8c0e343b2c 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueue.tsx @@ -1,11 +1,30 @@ -import { Box, Button, Flex, Heading } from '@invoke-ai/ui-library'; +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { + Badge, + Box, + Button, + ButtonGroup, + Flex, + Heading, + IconButton, + Menu, + MenuButton, + MenuItem, + MenuList, + Table, + TableContainer, + Tbody, + Th, + Thead, + Tr, +} from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { getApiErrorDetail } from 'features/modelManagerV2/util/getApiErrorDetail'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { memo, useCallback, useMemo, useRef, useState } from 'react'; -import { PiPauseBold, PiPlayBold, PiXBold } from 'react-icons/pi'; +import { PiBroomBold, PiCaretDownBold, PiPauseBold, PiPlayBold, PiXBold } from 'react-icons/pi'; import { useCancelModelInstallMutation, useListModelInstallsQuery, @@ -22,6 +41,18 @@ const hasRestartRequired = (job: ModelInstallJob) => { return job.download_parts?.some((part) => part.resume_required || part.status === 'error') ?? false; }; +const ModelQueueTableSx: SystemStyleObject = { + '& tbody tr:nth-of-type(odd)': { + backgroundColor: 'rgba(255, 255, 255, 0.04)', + }, + '& tbody tr:nth-of-type(even)': { + backgroundColor: 'transparent', + }, + 'td, th': { + borderColor: 'base.700', + }, +}; + export const ModelInstallQueue = memo(() => { const isConnected = useStore($isConnected); const { data } = useListModelInstallsQuery(); @@ -158,7 +189,6 @@ export const ModelInstallQueue = memo(() => { }, [data]); const pauseResumeLabel = showResumeAll ? t('modelManager.resumeAll') : t('modelManager.pauseAll'); - const pauseResumeTooltip = showResumeAll ? t('modelManager.resumeAllTooltip') : t('modelManager.pauseAllTooltip'); const pauseOrResumeAll = useCallback(() => { if (showResumeAll) { @@ -176,41 +206,19 @@ export const ModelInstallQueue = memo(() => { const isBulkActionRunning = bulkActionInProgress !== null; return ( - + + {/* Model Queue Header */} - {t('modelManager.installQueue')} - {!isConnected && ( - - - {t('modelManager.backendDisconnected')} - - - )} + {t('modelManager.installQueue')} + {!isConnected && {t('modelManager.backendDisconnected')}} - - - + + {/* Bulk Actions */} + - + + } /> + + : } + isDisabled={!pauseResumeAvailable || isBulkActionRunning || isPruning} + onClick={pauseOrResumeAll} + > + {pauseResumeLabel} + + } + isDisabled={!hasCancelableInstalls || isBulkActionRunning || isPruning} + onClick={cancelAll} + > + {t('modelManager.cancelAll')} + + + + - + + {/* Model Queue List */} + - - {data?.map((model) => ( - - ))} - + + + + + + + + + + + + {data?.map((model) => ( + + ))} + +
NameStatusActions
+
diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueBadge.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueBadge.tsx index 27096c2b52..4a5cd0cb68 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueBadge.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueBadge.tsx @@ -23,7 +23,7 @@ const ModelInstallQueueBadge = ({ status }: { status?: ModelInstallStatus }) => } return ( - + {t(statusConfig.translationKey)} ); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx index bd39030cdd..162f6b4ed5 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ModelInstallQueue/ModelInstallQueueItem.tsx @@ -1,10 +1,34 @@ -import { Badge, Box, Flex, IconButton, Progress, Text, Tooltip } from '@invoke-ai/ui-library'; +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { + Badge, + Box, + Button, + CircularProgress, + Flex, + Icon, + IconButton, + Td, + Text, + Tooltip, + Tr, +} from '@invoke-ai/ui-library'; import { isNil } from 'es-toolkit/compat'; import { getApiErrorDetail } from 'features/modelManagerV2/util/getApiErrorDetail'; import { toast } from 'features/toast/toast'; import { t } from 'i18next'; import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; -import { PiArrowClockwiseBold, PiPauseBold, PiPlayBold, PiXBold } from 'react-icons/pi'; +import { + PiArrowClockwiseBold, + PiCheckCircleFill, + PiLineVerticalBold, + PiPauseBold, + PiPlayBold, + PiWarningBold, + PiWarningDiamondBold, + PiWarningFill, + PiXBold, + PiXCircleBold, +} from 'react-icons/pi'; import { useCancelModelInstallMutation, usePauseModelInstallMutation, @@ -32,6 +56,34 @@ const formatBytes = (bytes: number) => { return `${bytes.toFixed(2)} ${units[i]}`; }; +const ProgressColumnSx: SystemStyleObject = { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}; + +const ModelNameColumnSx: SystemStyleObject = { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: 1, +}; + +const BadgesColumnSx: SystemStyleObject = { + display: 'flex', + gap: 1, + alignItems: 'flex-start', + flexWrap: 'wrap', +}; + +const ActionsColumnSx: SystemStyleObject = { + display: 'flex', + gap: 2, + alignItems: 'flex-start', + justifyContent: 'flex-end', + minWidth: '90px', +}; + export const ModelInstallQueueItem = memo((props: ModelListItemProps) => { const { installJob } = props; @@ -249,98 +301,147 @@ export const ModelInstallQueueItem = memo((props: ModelListItemProps) => { installJob.status === 'paused'; return ( - <> - - }> - - - {modelName} - - + {/* Progress */} + + + {hasRestartRequired ? ( + + + + ) : progressValue && progressValue < 100 ? ( + - - - - - {showResume && ( - } - onClick={handleResumeModelInstall} - variant="ghost" - /> - )} - {showPause && ( - } - onClick={handlePauseModelInstall} - variant="ghost" /> + ) : installJob.status === 'cancelled' ? ( + + + + ) : ( + + + )} + + + + {/* Model Info */} + + + {modelName} + + + {sourceLocation} + + {installJob.error_reason && ( + + {t('queue.failed')}: {installJob.error} + + )} + {hasRestartRequired && ( - } - onClick={handleRestartFailed} - variant="ghost" - /> + + {restartRequiredParts.map((part) => { + const fileName = part.source.split('/').slice(-1)[0] ?? t('common.unknown'); + const isResumeRequired = part.resume_required; + return ( + + + + {fileName} + + + {isResumeRequired ? t('modelManager.restartRequired') : t('queue.failed')} + + + {isResumeRequired ? t('modelManager.resumeRefused') : t('queue.failed')} + + } + onClick={getRestartFileHandler(part.source)} + variant="ghost" + ml="auto" + /> + + ); + })} + )} + + + + {/* Status */} + + + + {hasRestartRequired && ( + + {t('modelManager.restartRequired')} + + )} + + + + {/* Actions */} + + + {showResume ? ( + + ) : showPause ? ( + + ) : null} + + {hasRestartRequired && ( + + )} + {showCancel && ( } + aria-label={t('modelManager.cancel')} onClick={handleDeleteModelImport} - variant="ghost" + size="sm" + colorScheme="error" /> )} - {!showResume && !showPause && !showCancel && } - - - {hasRestartRequired && ( - - {restartRequiredParts.map((part) => { - const fileName = part.source.split('/').slice(-1)[0] ?? t('common.unknown'); - const isResumeRequired = part.resume_required; - return ( - - - {fileName} - - - {isResumeRequired ? t('modelManager.restartRequired') : t('queue.failed')} - - - {isResumeRequired ? t('modelManager.resumeRefused') : t('queue.failed')} - - } - onClick={getRestartFileHandler(part.source)} - variant="ghost" - /> - - ); - })} - - )} - + + {!showCancel && !showPause && !showResume && No actions available} + + + ); }); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx index 9039c0f85f..7957b31c2a 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/InstallModels.tsx @@ -1,5 +1,5 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { Box, Divider, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { $installModelsTabIndex } from 'features/modelManagerV2/store/installModelsStore'; import { StarterModelsForm } from 'features/modelManagerV2/subpanels/AddModelPanel/StarterModels/StarterModelsForm'; @@ -88,7 +88,8 @@ export const InstallModels = memo(() => { - + +