From 1994562bde15cd2533a9b4ac260f0f04c8427306 Mon Sep 17 00:00:00 2001 From: Juan Denis Date: Wed, 12 Aug 2026 11:09:13 -0400 Subject: [PATCH] refactor(cron): drop the status segments -- the column menu already does that With per-column filters the page carried two filtering UIs for the same thing: a segmented All/Active/Failed/Paused row, and the Status column's own menu with live per-value counts. The segments were the weaker of the two -- fixed buckets, no combining, and invisible in the chip bar -- so they go. The presets that used to set the segment now carry real column rules, which also makes them combinable with anything else you filter on. `status` gains an explicit `value` accessor and `type: 'enum'` so filtering and grouping key off the label the pill shows rather than an inferred guess. Same duplication exists on Servers (status + group), Services and Workspaces; those follow. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/pages/CronJobs.jsx | 53 ++++++++++++++------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/frontend/src/pages/CronJobs.jsx b/frontend/src/pages/CronJobs.jsx index de65115f..9bd944db 100644 --- a/frontend/src/pages/CronJobs.jsx +++ b/frontend/src/pages/CronJobs.jsx @@ -12,7 +12,7 @@ import { import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { - Pill, SegControl, SearchField, DataTable, Drawer, + Pill, SearchField, DataTable, Drawer, DataTableFooter, ListToolbar, } from '@/components/ds'; import { @@ -54,17 +54,22 @@ const describeSchedule = (job) => ( job.schedule_human || SCHEDULE_LABELS[job.schedule] || job.schedule ); -// Built-in saved views. State shape: { filter, search, sorts, hiddenKeys } — -// `filter` values are the SegControl options below, `sorts` use real column keys. +// Built-in saved views. +// `status` values are the labels jobState() produces: Healthy, Failed, Paused, +// No runs yet, Untracked. There is no separate segment filter any more — the +// Status column's own menu is the one place jobs are narrowed by state. +const RULES = (...value) => ({ match: 'all', rules: [{ id: 'st', field: 'status', op: 'any', value }] }); +const NO_RULES = { match: 'all', rules: [] }; + const BUILTIN_VIEWS = [ - { name: 'Paused', state: { filter: 'paused', search: '', sorts: [], hiddenKeys: [] } }, - { name: 'Active', state: { filter: 'active', search: '', sorts: [], hiddenKeys: [] } }, - { name: 'Recently run', state: { filter: 'all', search: '', sorts: [{ key: 'last_run', direction: 'desc' }], hiddenKeys: [] } }, + { name: 'Paused', state: { search: '', sorts: [], hiddenKeys: [], columnFilters: RULES('Paused') } }, + { name: 'Active', state: { search: '', sorts: [], hiddenKeys: [], columnFilters: RULES('Healthy', 'No runs yet', 'Untracked') } }, + { name: 'Recently run', state: { search: '', sorts: [{ key: 'last_run', direction: 'desc' }], hiddenKeys: [], columnFilters: NO_RULES } }, { // What broke, most recent failure first. name: 'Failing now', state: { - filter: 'failed', search: '', hiddenKeys: ['next_run'], + search: '', hiddenKeys: ['next_run'], columnFilters: RULES('Failed'), sorts: [{ key: 'last_run', direction: 'desc' }], }, }, @@ -73,14 +78,14 @@ const BUILTIN_VIEWS = [ // that look healthy only because nothing has checked them. name: 'Stale schedules', state: { - filter: 'active', search: '', hiddenKeys: [], + search: '', hiddenKeys: [], columnFilters: RULES('Healthy'), sorts: [{ key: 'last_run', direction: 'asc' }], }, }, { name: 'Health roll-call', state: { - filter: 'all', search: '', hiddenKeys: ['schedule'], + search: '', hiddenKeys: ['schedule'], columnFilters: NO_RULES, sorts: [{ key: 'status', direction: 'asc' }, { key: 'name', direction: 'asc' }], }, }, @@ -144,7 +149,6 @@ const CronJobs = () => { const [error, setError] = useState(null); // List filters (client-side, over already-loaded jobs) - const [filter, setFilter] = useState('all'); const [search, setSearch] = useState(''); // Table sort + column visibility (persisted, mirrored by the toolbar menus) @@ -156,9 +160,8 @@ const CronJobs = () => { // Shared list chrome: view picker + filter chips + filter drawer + tools, // driven off this page's existing sorts/hiddenKeys state. Same hook every // other list page uses, so /cron looks and behaves like /domains. - const extraViewState = useMemo(() => ({ filter, search }), [filter, search]); + const extraViewState = useMemo(() => ({ search }), [search]); const applyExtraViewState = useCallback((state) => { - if (state.filter !== undefined) setFilter(state.filter); if (state.search !== undefined) setSearch(state.search); }, []); @@ -279,24 +282,10 @@ const CronJobs = () => { const pausedCount = jobs.length - enabledCount; const q = search.trim().toLowerCase(); - const shown = jobs.filter((job) => { - const state = jobState(job); - const matchesFilter = filter === 'all' - || (filter === 'failed' ? state.key === 'failed' - : filter === 'paused' ? state.key === 'paused' - : state.key === 'active'); - const matchesQuery = !q - || (job.name || '').toLowerCase().includes(q) - || (job.command || '').toLowerCase().includes(q); - return matchesFilter && matchesQuery; - }); + const shown = jobs.filter((job) => !q + || (job.name || '').toLowerCase().includes(q) + || (job.command || '').toLowerCase().includes(q)); - const filterOptions = [ - { value: 'all', label: 'All', count: jobs.length }, - { value: 'active', label: 'Active', count: enabledCount - failedCount }, - { value: 'failed', label: 'Failed', count: failedCount }, - { value: 'paused', label: 'Paused', count: pausedCount }, - ]; const serviceNote = status?.available === false ? 'Cron service unavailable' @@ -361,6 +350,11 @@ const CronJobs = () => { key: 'status', header: 'Status', sortable: true, + type: 'enum', + groupable: true, + // The header menu filters and groups on the LABEL, which is what the + // pill shows — so a preset reads the same way the row does. + value: (job) => jobState(job).label, sortValue: (job) => jobState(job).label, render: (job) => { const state = jobState(job); @@ -477,7 +471,6 @@ const CronJobs = () => { onCreate={chrome.createView} /> } count={serviceNote && {serviceNote}} tools={( <>