1- import { Box , Button , Flex , Heading } from '@invoke-ai/ui-library' ;
2- import { useStore } from '@nanostores/react' ;
1+ import type { SystemStyleObject } from '@invoke-ai/ui-library' ;
2+ import {
3+ Box ,
4+ Button ,
5+ ButtonGroup ,
6+ Flex ,
7+ Heading ,
8+ IconButton ,
9+ Menu ,
10+ MenuButton ,
11+ MenuItem ,
12+ MenuList ,
13+ Table ,
14+ Tbody ,
15+ Td ,
16+ Text ,
17+ Th ,
18+ Thead ,
19+ Tr ,
20+ } from '@invoke-ai/ui-library' ;
321import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent' ;
422import { getApiErrorDetail } from 'features/modelManagerV2/util/getApiErrorDetail' ;
523import { toast } from 'features/toast/toast' ;
6- import { t } from 'i18next' ;
724import { memo , useCallback , useMemo , useRef , useState } from 'react' ;
8- import { PiPauseBold , PiPlayBold , PiXBold } from 'react-icons/pi' ;
25+ import { useTranslation } from 'react-i18next' ;
26+ import { PiBroomBold , PiCaretDownBold , PiPauseFill , PiPlayFill , PiXBold } from 'react-icons/pi' ;
927import {
1028 useCancelModelInstallMutation ,
1129 useListModelInstallsQuery ,
@@ -14,20 +32,56 @@ import {
1432 useResumeModelInstallMutation ,
1533} from 'services/api/endpoints/models' ;
1634import type { ModelInstallJob } from 'services/api/types' ;
17- import { $isConnected } from 'services/events/stores' ;
1835
1936import { ModelInstallQueueItem } from './ModelInstallQueueItem' ;
2037
2138const hasRestartRequired = ( job : ModelInstallJob ) => {
2239 return job . download_parts ?. some ( ( part ) => part . resume_required || part . status === 'error' ) ?? false ;
2340} ;
2441
42+ const ModelQueueTableSx : SystemStyleObject = {
43+ '& tbody tr:nth-of-type(odd)' : {
44+ backgroundColor : 'rgba(255, 255, 255, 0.04)' ,
45+ } ,
46+ '& tbody tr:nth-of-type(even)' : {
47+ backgroundColor : 'transparent' ,
48+ } ,
49+ 'td, th' : {
50+ borderColor : 'base.700' ,
51+ } ,
52+
53+ th : {
54+ position : 'sticky' ,
55+ top : 0 ,
56+ zIndex : 1 ,
57+ backgroundColor : 'base.800' ,
58+ py : 2 ,
59+ } ,
60+
61+ 'th:first-of-type' : {
62+ borderTopLeftRadius : 'base' ,
63+ } ,
64+ 'th:last-of-type' : {
65+ borderTopRightRadius : 'base' ,
66+ } ,
67+ 'tr:last-of-type td:first-of-type' : {
68+ borderBottomLeftRadius : 'base' ,
69+ } ,
70+ 'tr:last-of-type td:last-of-type' : {
71+ borderBottomRightRadius : 'base' ,
72+ } ,
73+ } ;
74+
2575export const ModelInstallQueue = memo ( ( ) => {
26- const isConnected = useStore ( $isConnected ) ;
76+ const { t } = useTranslation ( ) ;
2777 const { data } = useListModelInstallsQuery ( ) ;
2878 const [ bulkActionInProgress , setBulkActionInProgress ] = useState < 'pause' | 'resume' | 'cancel' | null > ( null ) ;
2979 const bulkActionLockRef = useRef ( false ) ;
3080
81+ const reversedData = useMemo ( ( ) => {
82+ return data ?. toReversed ( ) ?? [ ] ;
83+ } , [ data ] ) ;
84+
3185 const [ cancelModelInstall ] = useCancelModelInstallMutation ( ) ;
3286 const [ pauseModelInstall ] = usePauseModelInstallMutation ( ) ;
3387 const [ resumeModelInstall ] = useResumeModelInstallMutation ( ) ;
@@ -51,7 +105,7 @@ export const ModelInstallQueue = memo(() => {
51105 continue ;
52106 }
53107
54- if ( model . status === 'running' ) {
108+ if ( model . status === 'running' || model . status === 'downloads_done' ) {
55109 cancelable . push ( model . id ) ;
56110 }
57111 }
@@ -121,7 +175,7 @@ export const ModelInstallQueue = memo(() => {
121175 setBulkActionInProgress ( null ) ;
122176 }
123177 } ,
124- [ cancelModelInstall , isPruning , pauseModelInstall , resumeModelInstall ]
178+ [ cancelModelInstall , isPruning , pauseModelInstall , resumeModelInstall , t ]
125179 ) ;
126180
127181 const pruneCompletedModelInstalls = useCallback ( async ( ) => {
@@ -143,31 +197,25 @@ export const ModelInstallQueue = memo(() => {
143197 status : 'error' ,
144198 } ) ;
145199 }
146- } , [ _pruneCompletedModelInstalls ] ) ;
200+ } , [ _pruneCompletedModelInstalls , t ] ) ;
147201
148202 const hasPauseableInstalls = pauseableInstallIds . length > 0 ;
149203 const hasResumableInstalls = resumableInstallIds . length > 0 ;
150204 const hasCancelableInstalls = cancelableInstallIds . length > 0 ;
151- const showResumeAll = ! hasPauseableInstalls && hasResumableInstalls ;
152- const pauseResumeAvailable = hasPauseableInstalls || hasResumableInstalls ;
153205
154206 const pruneAvailable = useMemo ( ( ) => {
155207 return data ?. some (
156208 ( model ) => model . status === 'cancelled' || model . status === 'error' || model . status === 'completed'
157209 ) ;
158210 } , [ data ] ) ;
159211
160- const pauseResumeLabel = showResumeAll ? t ( 'modelManager.resumeAll' ) : t ( 'modelManager.pauseAll' ) ;
161- const pauseResumeTooltip = showResumeAll ? t ( 'modelManager.resumeAllTooltip' ) : t ( 'modelManager.pauseAllTooltip' ) ;
162-
163- const pauseOrResumeAll = useCallback ( ( ) => {
164- if ( showResumeAll ) {
165- void runBulkAction ( 'resume' , resumableInstallIds ) ;
166- return ;
167- }
168-
212+ const pauseAll = useCallback ( ( ) => {
169213 void runBulkAction ( 'pause' , pauseableInstallIds ) ;
170- } , [ pauseableInstallIds , resumableInstallIds , runBulkAction , showResumeAll ] ) ;
214+ } , [ pauseableInstallIds , runBulkAction ] ) ;
215+
216+ const resumeAll = useCallback ( ( ) => {
217+ void runBulkAction ( 'resume' , resumableInstallIds ) ;
218+ } , [ resumableInstallIds , runBulkAction ] ) ;
171219
172220 const cancelAll = useCallback ( ( ) => {
173221 void runBulkAction ( 'cancel' , cancelableInstallIds ) ;
@@ -176,57 +224,101 @@ export const ModelInstallQueue = memo(() => {
176224 const isBulkActionRunning = bulkActionInProgress !== null ;
177225
178226 return (
179- < Flex flexDir = "column" p = { 3 } h = "full" gap = { 3 } >
227+ < Flex flexDir = "column" h = "full" gap = { 4 } >
228+ { /* Model Queue Header */ }
180229 < Flex justifyContent = "space-between" alignItems = "center" >
181230 < Flex alignItems = "center" gap = { 2 } >
182- < Heading size = "sm" > { t ( 'modelManager.installQueue' ) } </ Heading >
183- { ! isConnected && (
184- < Box layerStyle = "first" px = { 2 } py = { 0.5 } borderRadius = "base" >
185- < Heading size = "sm" color = "error.300" >
186- { t ( 'modelManager.backendDisconnected' ) }
187- </ Heading >
188- </ Box >
189- ) }
231+ < Heading size = "md" > { t ( 'modelManager.installQueue' ) } </ Heading >
190232 </ Flex >
191- < Flex gap = { 2 } alignItems = "center" >
192- < Button
193- size = "sm"
194- leftIcon = { showResumeAll ? < PiPlayBold /> : < PiPauseBold /> }
195- isDisabled = { ! pauseResumeAvailable || isBulkActionRunning || isPruning }
196- isLoading = { bulkActionInProgress === 'pause' || bulkActionInProgress === 'resume' }
197- onClick = { pauseOrResumeAll }
198- tooltip = { pauseResumeTooltip }
199- >
200- { pauseResumeLabel }
201- </ Button >
202- < Button
203- size = "sm"
204- leftIcon = { < PiXBold /> }
205- isDisabled = { ! hasCancelableInstalls || isBulkActionRunning || isPruning }
206- isLoading = { bulkActionInProgress === 'cancel' }
207- onClick = { cancelAll }
208- tooltip = { t ( 'modelManager.cancelAllTooltip' ) }
209- >
210- { t ( 'modelManager.cancelAll' ) }
211- </ Button >
212- < Button
213- size = "sm"
214- isDisabled = { ! pruneAvailable || isBulkActionRunning }
215- isLoading = { isPruning }
216- onClick = { pruneCompletedModelInstalls }
217- tooltip = { t ( 'modelManager.pruneTooltip' ) }
218- >
219- { t ( 'modelManager.prune' ) }
220- </ Button >
233+
234+ { /* Bulk Actions */ }
235+ { /* Non-destructive, easily-ccessible actions */ }
236+ < Flex gap = { 2 } >
237+ { hasPauseableInstalls && (
238+ < Button
239+ size = "sm"
240+ leftIcon = { < PiPauseFill /> }
241+ isDisabled = { isBulkActionRunning || isPruning }
242+ onClick = { pauseAll }
243+ variant = "outline"
244+ >
245+ { t ( 'modelManager.pauseAll' ) }
246+ </ Button >
247+ ) }
248+
249+ { hasResumableInstalls && (
250+ < Button
251+ size = "sm"
252+ leftIcon = { < PiPlayFill /> }
253+ isDisabled = { isBulkActionRunning || isPruning }
254+ onClick = { resumeAll }
255+ variant = "outline"
256+ >
257+ { t ( 'modelManager.resumeAll' ) }
258+ </ Button >
259+ ) }
260+
261+ { /* Destructive Actions go to the button group/menu */ }
262+ < ButtonGroup >
263+ < Button
264+ leftIcon = { < PiBroomBold /> }
265+ size = "sm"
266+ isDisabled = { ! pruneAvailable || isBulkActionRunning || isPruning }
267+ onClick = { pruneCompletedModelInstalls }
268+ variant = "outline"
269+ >
270+ { t ( 'modelManager.prune' ) }
271+ </ Button >
272+ < Menu >
273+ < MenuButton
274+ as = { IconButton }
275+ size = "sm"
276+ aria-label = { t ( 'accessibility.menu' ) }
277+ icon = { < PiCaretDownBold /> }
278+ disabled = { ! pruneAvailable && ! hasCancelableInstalls }
279+ />
280+ < MenuList >
281+ < MenuItem
282+ color = "error.300"
283+ icon = { < PiXBold /> }
284+ isDisabled = { ! hasCancelableInstalls || isBulkActionRunning || isPruning }
285+ onClick = { cancelAll }
286+ isDestructive
287+ >
288+ { t ( 'modelManager.cancelAll' ) }
289+ </ MenuItem >
290+ </ MenuList >
291+ </ Menu >
292+ </ ButtonGroup >
221293 </ Flex >
222294 </ Flex >
223- < Box layerStyle = "first" p = { 3 } borderRadius = "base" w = "full" h = "full" >
295+
296+ { /* Model Queue List */ }
297+ < Box layerStyle = "second" borderRadius = "base" w = "full" h = "full" >
224298 < ScrollableContent >
225- < Flex flexDir = "column-reverse" gap = "2" w = "full" >
226- { data ?. map ( ( model ) => (
227- < ModelInstallQueueItem key = { model . id } installJob = { model } />
228- ) ) }
229- </ Flex >
299+ < Table size = "sm" sx = { ModelQueueTableSx } >
300+ < Thead >
301+ < Tr >
302+ < Th minWidth = "50px" > </ Th >
303+ < Th width = "80%" > Name</ Th >
304+ < Th minWidth = "130px" > Status</ Th >
305+ < Th minWidth = "160px" textAlign = "right" >
306+ Actions
307+ </ Th >
308+ </ Tr >
309+ </ Thead >
310+ < Tbody >
311+ { data ?. length === 0 ? (
312+ < Tr >
313+ < Td colSpan = { 4 } textAlign = "center" py = { 8 } >
314+ < Text variant = "subtext" > { t ( 'modelManager.queueEmpty' ) } </ Text >
315+ </ Td >
316+ </ Tr >
317+ ) : (
318+ reversedData ?. map ( ( model ) => < ModelInstallQueueItem key = { model . id } installJob = { model } /> )
319+ ) }
320+ </ Tbody >
321+ </ Table >
230322 </ ScrollableContent >
231323 </ Box >
232324 </ Flex >
0 commit comments