Skip to content

Commit 2553cac

Browse files
committed
improvement(scheduled-tasks): use combobox filter panel matching logs UI style
1 parent 0142c69 commit 2553cac

1 file changed

Lines changed: 118 additions & 91 deletions

File tree

apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx

Lines changed: 118 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
import { useCallback, useMemo, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { 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'
715
import { Calendar } from '@/components/emcn/icons'
8-
import { cn } from '@/lib/core/utils/cn'
916
import { formatAbsoluteDate } from '@/lib/core/utils/formatting'
1017
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
1118
import 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

Comments
 (0)