33import { useCallback , useMemo , useState } from 'react'
44import { createLogger } from '@sim/logger'
55import { useParams } from 'next/navigation'
6- import { Button , Modal , ModalBody , ModalContent , ModalFooter , ModalHeader } from '@/components/emcn'
6+ import {
7+ Button ,
8+ Combobox ,
9+ Modal ,
10+ ModalBody ,
11+ ModalContent ,
12+ ModalFooter ,
13+ ModalHeader ,
14+ } from '@/components/emcn'
715import { Calendar } from '@/components/emcn/icons'
8- import { cn } from '@/lib/core/utils/cn'
916import { formatAbsoluteDate } from '@/lib/core/utils/formatting'
1017import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
1118import type {
@@ -84,9 +91,9 @@ export function ScheduledTasks() {
8491 column : string
8592 direction : 'asc' | 'desc'
8693 } | null > ( null )
87- const [ scheduleTypeFilter , setScheduleTypeFilter ] = useState < 'all' | 'recurring' | 'once' > ( 'all' )
88- const [ statusFilter , setStatusFilter ] = useState < 'all' | 'active' | 'paused' > ( 'all' )
89- const [ healthFilter , setHealthFilter ] = useState < 'all' | 'has-failures' > ( 'all' )
94+ const [ scheduleTypeFilter , setScheduleTypeFilter ] = useState < string [ ] > ( [ ] )
95+ const [ statusFilter , setStatusFilter ] = useState < string [ ] > ( [ ] )
96+ const [ healthFilter , setHealthFilter ] = useState < string [ ] > ( [ ] )
9097
9198 const visibleItems = useMemo (
9299 ( ) => allItems . filter ( ( item ) => item . sourceType === 'job' && item . status !== 'completed' ) ,
@@ -104,19 +111,23 @@ export function ScheduledTasks() {
104111 } )
105112 : visibleItems
106113
107- if ( scheduleTypeFilter !== 'all' ) {
108- result = result . filter ( ( item ) =>
109- scheduleTypeFilter === 'recurring' ? Boolean ( item . cronExpression ) : ! item . cronExpression
110- )
114+ if ( scheduleTypeFilter . length > 0 ) {
115+ result = result . filter ( ( item ) => {
116+ if ( scheduleTypeFilter . includes ( 'recurring' ) && Boolean ( item . cronExpression ) ) return true
117+ if ( scheduleTypeFilter . includes ( 'once' ) && ! item . cronExpression ) return true
118+ return false
119+ } )
111120 }
112121
113- if ( statusFilter !== 'all' ) {
114- result = result . filter ( ( item ) =>
115- statusFilter === 'active' ? item . status === 'active' : item . status === 'disabled'
116- )
122+ if ( statusFilter . length > 0 ) {
123+ result = result . filter ( ( item ) => {
124+ if ( statusFilter . includes ( 'active' ) && item . status === 'active' ) return true
125+ if ( statusFilter . includes ( 'paused' ) && item . status === 'disabled' ) return true
126+ return false
127+ } )
117128 }
118129
119- if ( healthFilter === 'has-failures' ) {
130+ if ( healthFilter . length > 0 && healthFilter . includes ( 'has-failures' ) ) {
120131 result = result . filter ( ( item ) => ( item . failedCount ?? 0 ) > 0 )
121132 }
122133
@@ -239,101 +250,117 @@ export function ScheduledTasks() {
239250 [ activeSort ]
240251 )
241252
253+ const scheduleTypeDisplayLabel = useMemo ( ( ) => {
254+ if ( scheduleTypeFilter . length === 0 ) return 'All'
255+ if ( scheduleTypeFilter . length === 1 )
256+ return scheduleTypeFilter [ 0 ] === 'recurring' ? 'Recurring' : 'One-time'
257+ return `${ scheduleTypeFilter . length } selected`
258+ } , [ scheduleTypeFilter ] )
259+
260+ const statusDisplayLabel = useMemo ( ( ) => {
261+ if ( statusFilter . length === 0 ) return 'All'
262+ if ( statusFilter . length === 1 ) return statusFilter [ 0 ] === 'active' ? 'Active' : 'Paused'
263+ return `${ statusFilter . length } selected`
264+ } , [ statusFilter ] )
265+
266+ const healthDisplayLabel = useMemo ( ( ) => {
267+ if ( healthFilter . length === 0 ) return 'All'
268+ return 'Has failures'
269+ } , [ healthFilter ] )
270+
271+ const hasActiveFilters =
272+ scheduleTypeFilter . length > 0 || statusFilter . length > 0 || healthFilter . length > 0
273+
242274 const filterContent = (
243- < div className = 'w-[200px] ' >
244- < div className = 'border-[var(--border-1)] border-b px-3 py-2 ' >
275+ < div className = 'flex w-[240px] flex-col gap-3 p-3 ' >
276+ < div className = 'flex flex-col gap-1.5 ' >
245277 < span className = 'font-medium text-[var(--text-secondary)] text-caption' > Schedule Type</ span >
246- </ div >
247- < div className = 'flex flex-col gap-0.5 px-3 py-2' >
248- { (
249- [
250- { value : 'all' , label : 'All' } ,
278+ < Combobox
279+ options = { [
251280 { value : 'recurring' , label : 'Recurring' } ,
252281 { value : 'once' , label : 'One-time' } ,
253- ] as const
254- ) . map ( ( { value, label } ) => (
255- < button
256- key = { value }
257- type = 'button'
258- className = { cn (
259- 'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]' ,
260- scheduleTypeFilter === value && 'bg-[var(--surface-active)]'
261- ) }
262- onClick = { ( ) => setScheduleTypeFilter ( value ) }
263- >
264- { label }
265- </ button >
266- ) ) }
282+ ] }
283+ multiSelect
284+ multiSelectValues = { scheduleTypeFilter }
285+ onMultiSelectChange = { setScheduleTypeFilter }
286+ overlayContent = {
287+ < span className = 'truncate text-[var(--text-primary)]' > { scheduleTypeDisplayLabel } </ span >
288+ }
289+ showAllOption
290+ allOptionLabel = 'All'
291+ size = 'sm'
292+ className = 'h-[32px] w-full rounded-md'
293+ />
267294 </ div >
268- < div className = 'border-[var(--border-1)] border-t border-b px-3 py-2 ' >
295+ < div className = 'flex flex-col gap-1.5 ' >
269296 < span className = 'font-medium text-[var(--text-secondary)] text-caption' > Status</ span >
270- </ div >
271- < div className = 'flex flex-col gap-0.5 px-3 py-2' >
272- { (
273- [
274- { value : 'all' , label : 'All' } ,
297+ < Combobox
298+ options = { [
275299 { value : 'active' , label : 'Active' } ,
276300 { value : 'paused' , label : 'Paused' } ,
277- ] as const
278- ) . map ( ( { value, label } ) => (
279- < button
280- key = { value }
281- type = 'button'
282- className = { cn (
283- 'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]' ,
284- statusFilter === value && 'bg-[var(--surface-active)]'
285- ) }
286- onClick = { ( ) => setStatusFilter ( value ) }
287- >
288- { label }
289- </ button >
290- ) ) }
301+ ] }
302+ multiSelect
303+ multiSelectValues = { statusFilter }
304+ onMultiSelectChange = { setStatusFilter }
305+ overlayContent = {
306+ < span className = 'truncate text-[var(--text-primary)]' > { statusDisplayLabel } </ span >
307+ }
308+ showAllOption
309+ allOptionLabel = 'All'
310+ size = 'sm'
311+ className = 'h-[32px] w-full rounded-md'
312+ />
291313 </ div >
292- < div className = 'border-[var(--border-1)] border-t border-b px-3 py-2 ' >
314+ < div className = 'flex flex-col gap-1.5 ' >
293315 < span className = 'font-medium text-[var(--text-secondary)] text-caption' > Health</ span >
316+ < Combobox
317+ options = { [ { value : 'has-failures' , label : 'Has failures' } ] }
318+ multiSelect
319+ multiSelectValues = { healthFilter }
320+ onMultiSelectChange = { setHealthFilter }
321+ overlayContent = {
322+ < span className = 'truncate text-[var(--text-primary)]' > { healthDisplayLabel } </ span >
323+ }
324+ showAllOption
325+ allOptionLabel = 'All'
326+ size = 'sm'
327+ className = 'h-[32px] w-full rounded-md'
328+ />
294329 </ div >
295- < div className = 'flex flex-col gap-0.5 px-3 py-2' >
296- { (
297- [
298- { value : 'all' , label : 'All' } ,
299- { value : 'has-failures' , label : 'Has failures' } ,
300- ] as const
301- ) . map ( ( { value, label } ) => (
302- < button
303- key = { value }
304- type = 'button'
305- className = { cn (
306- 'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]' ,
307- healthFilter === value && 'bg-[var(--surface-active)]'
308- ) }
309- onClick = { ( ) => setHealthFilter ( value ) }
310- >
311- { label }
312- </ button >
313- ) ) }
314- </ div >
330+ { hasActiveFilters && (
331+ < button
332+ type = 'button'
333+ onClick = { ( ) => {
334+ setScheduleTypeFilter ( [ ] )
335+ setStatusFilter ( [ ] )
336+ setHealthFilter ( [ ] )
337+ } }
338+ className = 'flex h-[32px] w-full items-center justify-center rounded-md text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-active)]'
339+ >
340+ Clear all filters
341+ </ button >
342+ ) }
315343 </ div >
316344 )
317345
318346 const filterTags : FilterTag [ ] = useMemo ( ( ) => {
319347 const tags : FilterTag [ ] = [ ]
320- if ( scheduleTypeFilter !== 'all' ) {
321- tags . push ( {
322- label : scheduleTypeFilter === 'recurring' ? 'Type: Recurring' : 'Type: One-time' ,
323- onRemove : ( ) => setScheduleTypeFilter ( 'all' ) ,
324- } )
348+ if ( scheduleTypeFilter . length > 0 ) {
349+ const label =
350+ scheduleTypeFilter . length === 1
351+ ? `Type: ${ scheduleTypeFilter [ 0 ] === 'recurring' ? 'Recurring' : 'One-time' } `
352+ : `Type: ${ scheduleTypeFilter . length } selected`
353+ tags . push ( { label, onRemove : ( ) => setScheduleTypeFilter ( [ ] ) } )
325354 }
326- if ( statusFilter !== 'all' ) {
327- tags . push ( {
328- label : statusFilter === 'active' ? 'Status: Active' : 'Status: Paused' ,
329- onRemove : ( ) => setStatusFilter ( 'all' ) ,
330- } )
355+ if ( statusFilter . length > 0 ) {
356+ const label =
357+ statusFilter . length === 1
358+ ? `Status: ${ statusFilter [ 0 ] === 'active' ? 'Active' : 'Paused' } `
359+ : `Status: ${ statusFilter . length } selected`
360+ tags . push ( { label, onRemove : ( ) => setStatusFilter ( [ ] ) } )
331361 }
332- if ( healthFilter === 'has-failures' ) {
333- tags . push ( {
334- label : 'Health: Has failures' ,
335- onRemove : ( ) => setHealthFilter ( 'all' ) ,
336- } )
362+ if ( healthFilter . length > 0 ) {
363+ tags . push ( { label : 'Health: Has failures' , onRemove : ( ) => setHealthFilter ( [ ] ) } )
337364 }
338365 return tags
339366 } , [ scheduleTypeFilter , statusFilter , healthFilter ] )
0 commit comments