mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(model manager): redesign queue
This commit is contained in:
@@ -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 (
|
||||
<Flex flexDir="column" p={3} h="full" gap={3}>
|
||||
<Flex flexDir="column" h="full" gap={4}>
|
||||
{/* Model Queue Header */}
|
||||
<Flex justifyContent="space-between" alignItems="center">
|
||||
<Flex alignItems="center" gap={2}>
|
||||
<Heading size="sm">{t('modelManager.installQueue')}</Heading>
|
||||
{!isConnected && (
|
||||
<Box layerStyle="first" px={2} py={0.5} borderRadius="base">
|
||||
<Heading size="sm" color="error.300">
|
||||
{t('modelManager.backendDisconnected')}
|
||||
</Heading>
|
||||
</Box>
|
||||
)}
|
||||
<Heading size="md">{t('modelManager.installQueue')}</Heading>
|
||||
{!isConnected && <Badge colorScheme="red">{t('modelManager.backendDisconnected')}</Badge>}
|
||||
</Flex>
|
||||
<Flex gap={2} alignItems="center">
|
||||
<Button
|
||||
size="sm"
|
||||
leftIcon={showResumeAll ? <PiPlayBold /> : <PiPauseBold />}
|
||||
isDisabled={!pauseResumeAvailable || isBulkActionRunning || isPruning}
|
||||
isLoading={bulkActionInProgress === 'pause' || bulkActionInProgress === 'resume'}
|
||||
onClick={pauseOrResumeAll}
|
||||
tooltip={pauseResumeTooltip}
|
||||
>
|
||||
{pauseResumeLabel}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
leftIcon={<PiXBold />}
|
||||
isDisabled={!hasCancelableInstalls || isBulkActionRunning || isPruning}
|
||||
isLoading={bulkActionInProgress === 'cancel'}
|
||||
onClick={cancelAll}
|
||||
tooltip={t('modelManager.cancelAllTooltip')}
|
||||
>
|
||||
{t('modelManager.cancelAll')}
|
||||
</Button>
|
||||
|
||||
{/* Bulk Actions */}
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
size="sm"
|
||||
leftIcon={<PiBroomBold />}
|
||||
isDisabled={!pruneAvailable || isBulkActionRunning}
|
||||
isLoading={isPruning}
|
||||
onClick={pruneCompletedModelInstalls}
|
||||
@@ -218,15 +226,48 @@ export const ModelInstallQueue = memo(() => {
|
||||
>
|
||||
{t('modelManager.prune')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Menu>
|
||||
<MenuButton as={IconButton} size="sm" icon={<PiCaretDownBold />} />
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
icon={showResumeAll ? <PiPlayBold /> : <PiPauseBold />}
|
||||
isDisabled={!pauseResumeAvailable || isBulkActionRunning || isPruning}
|
||||
onClick={pauseOrResumeAll}
|
||||
>
|
||||
{pauseResumeLabel}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<PiXBold />}
|
||||
isDisabled={!hasCancelableInstalls || isBulkActionRunning || isPruning}
|
||||
onClick={cancelAll}
|
||||
>
|
||||
{t('modelManager.cancelAll')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
<Box layerStyle="first" p={3} borderRadius="base" w="full" h="full">
|
||||
|
||||
{/* Model Queue List */}
|
||||
<Box layerStyle="second" py={4} borderRadius="base" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
<Flex flexDir="column-reverse" gap="2" w="full">
|
||||
{data?.map((model) => (
|
||||
<ModelInstallQueueItem key={model.id} installJob={model} />
|
||||
))}
|
||||
</Flex>
|
||||
<TableContainer>
|
||||
<Table size="sm" sx={ModelQueueTableSx}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th></Th>
|
||||
<Th>Name</Th>
|
||||
<Th>Status</Th>
|
||||
<Th textAlign="right">Actions</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data?.map((model) => (
|
||||
<ModelInstallQueueItem key={model.id} installJob={model} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -23,7 +23,7 @@ const ModelInstallQueueBadge = ({ status }: { status?: ModelInstallStatus }) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge textAlign="center" w="134px" colorScheme={statusConfig.colorScheme}>
|
||||
<Badge textAlign="center" colorScheme={statusConfig.colorScheme}>
|
||||
{t(statusConfig.translationKey)}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Flex gap={1} w="full" alignItems="center">
|
||||
<Tooltip maxW={600} label={<TooltipLabel name={modelName} source={sourceLocation} installJob={installJob} />}>
|
||||
<Flex gap={3} w="full" alignItems="center">
|
||||
<Text w={96} whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis">
|
||||
{modelName}
|
||||
</Text>
|
||||
<Progress
|
||||
w="full"
|
||||
flexGrow={1}
|
||||
<Tr>
|
||||
{/* Progress */}
|
||||
<Td>
|
||||
<Box sx={ProgressColumnSx}>
|
||||
{hasRestartRequired ? (
|
||||
<Box sx={{ color: 'orange.300' }}>
|
||||
<PiWarningBold size={20} />
|
||||
</Box>
|
||||
) : progressValue && progressValue < 100 ? (
|
||||
<CircularProgress
|
||||
size="20px"
|
||||
value={progressValue ?? 0}
|
||||
isIndeterminate={progressValue === null}
|
||||
aria-label={t('accessibility.invokeProgressBar')}
|
||||
h={2}
|
||||
/>
|
||||
<ModelInstallQueueBadge status={installJob.status} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<Flex gap={1} alignItems="center" justifyContent="flex-end" minW="90px">
|
||||
{showResume && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
tooltip={t('modelManager.resume')}
|
||||
aria-label={t('modelManager.resume')}
|
||||
icon={<PiPlayBold />}
|
||||
onClick={handleResumeModelInstall}
|
||||
variant="ghost"
|
||||
/>
|
||||
)}
|
||||
{showPause && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
tooltip={t('modelManager.pause')}
|
||||
aria-label={t('modelManager.pause')}
|
||||
icon={<PiPauseBold />}
|
||||
onClick={handlePauseModelInstall}
|
||||
variant="ghost"
|
||||
/>
|
||||
) : installJob.status === 'cancelled' ? (
|
||||
<Box sx={{ color: 'orange.200', transform: 'rotate(90deg)' }}>
|
||||
<PiLineVerticalBold size={20} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ color: 'green.300' }}>
|
||||
<PiCheckCircleFill size={24} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
|
||||
{/* Model Info */}
|
||||
<Td>
|
||||
<Box sx={ModelNameColumnSx}>
|
||||
<Text fontWeight="semibold">{modelName}</Text>
|
||||
<Box>
|
||||
<Text as="span" fontStyle="italic" wordBreak="break-all">
|
||||
{sourceLocation}
|
||||
</Text>
|
||||
{installJob.error_reason && (
|
||||
<Text as="span" color="error.500" ml={2}>
|
||||
{t('queue.failed')}: {installJob.error}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{hasRestartRequired && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
tooltip={t('modelManager.restartFailed')}
|
||||
aria-label={t('modelManager.restartFailed')}
|
||||
icon={<PiArrowClockwiseBold />}
|
||||
onClick={handleRestartFailed}
|
||||
variant="ghost"
|
||||
/>
|
||||
<Flex direction="column" gap={1} w="full" mt={1}>
|
||||
{restartRequiredParts.map((part) => {
|
||||
const fileName = part.source.split('/').slice(-1)[0] ?? t('common.unknown');
|
||||
const isResumeRequired = part.resume_required;
|
||||
return (
|
||||
<Flex key={part.source} gap={2} alignItems="center" wrap="wrap" p={2} bg="base.800" borderRadius="md">
|
||||
<Icon
|
||||
as={isResumeRequired ? PiWarningFill : PiWarningDiamondBold}
|
||||
color={isResumeRequired ? 'orange.500' : 'red.500'}
|
||||
/>
|
||||
<Text fontSize="xs" color="base.200" maxW="200px" noOfLines={1} title={fileName}>
|
||||
{fileName}
|
||||
</Text>
|
||||
<Badge colorScheme={isResumeRequired ? 'orange' : 'red'} fontSize="10px">
|
||||
{isResumeRequired ? t('modelManager.restartRequired') : t('queue.failed')}
|
||||
</Badge>
|
||||
<Text fontSize="xs" color="warning.400">
|
||||
{isResumeRequired ? t('modelManager.resumeRefused') : t('queue.failed')}
|
||||
</Text>
|
||||
<IconButton
|
||||
size="xs"
|
||||
tooltip={t('modelManager.restartFile')}
|
||||
aria-label={t('modelManager.restartFile')}
|
||||
icon={<PiArrowClockwiseBold />}
|
||||
onClick={getRestartFileHandler(part.source)}
|
||||
variant="ghost"
|
||||
ml="auto"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
|
||||
{/* Status */}
|
||||
<Td>
|
||||
<Box sx={BadgesColumnSx}>
|
||||
<ModelInstallQueueBadge status={installJob.status} />
|
||||
{hasRestartRequired && (
|
||||
<Tooltip label={t('modelManager.restartRequiredTooltip')}>
|
||||
<Badge colorScheme="red">{t('modelManager.restartRequired')}</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Td>
|
||||
|
||||
{/* Actions */}
|
||||
<Td textAlign="right" minWidth={130}>
|
||||
<Box sx={ActionsColumnSx}>
|
||||
{showResume ? (
|
||||
<Button
|
||||
size="sm"
|
||||
tooltip={t('modelManager.resume')}
|
||||
leftIcon={<PiPlayBold />}
|
||||
onClick={handleResumeModelInstall}
|
||||
>
|
||||
{t('modelManager.resume')}
|
||||
</Button>
|
||||
) : showPause ? (
|
||||
<Button
|
||||
size="sm"
|
||||
tooltip={t('modelManager.pause')}
|
||||
leftIcon={<PiPauseBold />}
|
||||
onClick={handlePauseModelInstall}
|
||||
>
|
||||
{t('modelManager.pause')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{hasRestartRequired && (
|
||||
<Button
|
||||
tooltip={t('modelManager.restartFailed')}
|
||||
size="sm"
|
||||
leftIcon={<PiArrowClockwiseBold />}
|
||||
onClick={handleRestartFailed}
|
||||
colorScheme="error"
|
||||
variant="ghost"
|
||||
>
|
||||
{t('modelManager.restartFailed')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showCancel && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
tooltip={t('modelManager.cancel')}
|
||||
aria-label={t('modelManager.cancel')}
|
||||
icon={<PiXBold />}
|
||||
aria-label={t('modelManager.cancel')}
|
||||
onClick={handleDeleteModelImport}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
colorScheme="error"
|
||||
/>
|
||||
)}
|
||||
{!showResume && !showPause && !showCancel && <Box w="24px" />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{hasRestartRequired && (
|
||||
<Flex direction="column" gap={1} w="full" mt={1}>
|
||||
{restartRequiredParts.map((part) => {
|
||||
const fileName = part.source.split('/').slice(-1)[0] ?? t('common.unknown');
|
||||
const isResumeRequired = part.resume_required;
|
||||
return (
|
||||
<Flex key={part.source} gap={2} alignItems="center" wrap="wrap">
|
||||
<Text fontSize="xs" color="base.200" maxW="200px" noOfLines={1}>
|
||||
{fileName}
|
||||
</Text>
|
||||
<Badge colorScheme={isResumeRequired ? 'orange' : 'red'} fontSize="10px">
|
||||
{isResumeRequired ? t('modelManager.restartRequired') : t('queue.failed')}
|
||||
</Badge>
|
||||
<Text fontSize="xs" color="warning.400">
|
||||
{isResumeRequired ? t('modelManager.resumeRefused') : t('queue.failed')}
|
||||
</Text>
|
||||
<IconButton
|
||||
size="xs"
|
||||
tooltip={t('modelManager.restartFile')}
|
||||
aria-label={t('modelManager.restartFile')}
|
||||
icon={<PiArrowClockwiseBold />}
|
||||
onClick={getRestartFileHandler(part.source)}
|
||||
variant="ghost"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
|
||||
{!showCancel && !showPause && !showResume && <Text fontSize="2xs">No actions available</Text>}
|
||||
</Box>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Box layerStyle="second" borderRadius="base" h="50%">
|
||||
<Divider />
|
||||
<Box h="65%">
|
||||
<ModelInstallQueue />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
Reference in New Issue
Block a user