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) <noreply@anthropic.com>
This commit is contained in:
Juan DenisandClaude Opus 5 committed 2026-08-12 11:09:13 -04:00
1 parent e659773bfc
commit 1994562bde
1 file changed
+23 -30
+23 -30
View File
@@ -12,7 +12,7 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { import {
Pill, SegControl, SearchField, DataTable, Drawer, Pill, SearchField, DataTable, Drawer,
DataTableFooter, ListToolbar, DataTableFooter, ListToolbar,
} from '@/components/ds'; } from '@/components/ds';
import { import {
@@ -54,17 +54,22 @@ const describeSchedule = (job) => (
job.schedule_human || SCHEDULE_LABELS[job.schedule] || job.schedule job.schedule_human || SCHEDULE_LABELS[job.schedule] || job.schedule
); );
// Built-in saved views. State shape: { filter, search, sorts, hiddenKeys } — // Built-in saved views.
// `filter` values are the SegControl options below, `sorts` use real column keys. // `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 = [ const BUILTIN_VIEWS = [
{ name: 'Paused', state: { filter: 'paused', search: '', sorts: [], hiddenKeys: [] } }, { name: 'Paused', state: { search: '', sorts: [], hiddenKeys: [], columnFilters: RULES('Paused') } },
{ name: 'Active', state: { filter: 'active', search: '', sorts: [], hiddenKeys: [] } }, { name: 'Active', state: { search: '', sorts: [], hiddenKeys: [], columnFilters: RULES('Healthy', 'No runs yet', 'Untracked') } },
{ name: 'Recently run', state: { filter: 'all', search: '', sorts: [{ key: 'last_run', direction: 'desc' }], hiddenKeys: [] } }, { name: 'Recently run', state: { search: '', sorts: [{ key: 'last_run', direction: 'desc' }], hiddenKeys: [], columnFilters: NO_RULES } },
{ {
// What broke, most recent failure first. // What broke, most recent failure first.
name: 'Failing now', name: 'Failing now',
state: { state: {
filter: 'failed', search: '', hiddenKeys: ['next_run'], search: '', hiddenKeys: ['next_run'], columnFilters: RULES('Failed'),
sorts: [{ key: 'last_run', direction: 'desc' }], sorts: [{ key: 'last_run', direction: 'desc' }],
}, },
}, },
@@ -73,14 +78,14 @@ const BUILTIN_VIEWS = [
// that look healthy only because nothing has checked them. // that look healthy only because nothing has checked them.
name: 'Stale schedules', name: 'Stale schedules',
state: { state: {
filter: 'active', search: '', hiddenKeys: [], search: '', hiddenKeys: [], columnFilters: RULES('Healthy'),
sorts: [{ key: 'last_run', direction: 'asc' }], sorts: [{ key: 'last_run', direction: 'asc' }],
}, },
}, },
{ {
name: 'Health roll-call', name: 'Health roll-call',
state: { state: {
filter: 'all', search: '', hiddenKeys: ['schedule'], search: '', hiddenKeys: ['schedule'], columnFilters: NO_RULES,
sorts: [{ key: 'status', direction: 'asc' }, { key: 'name', direction: 'asc' }], sorts: [{ key: 'status', direction: 'asc' }, { key: 'name', direction: 'asc' }],
}, },
}, },
@@ -144,7 +149,6 @@ const CronJobs = () => {
const [error, setError] = useState(null); const [error, setError] = useState(null);
// List filters (client-side, over already-loaded jobs) // List filters (client-side, over already-loaded jobs)
const [filter, setFilter] = useState('all');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
// Table sort + column visibility (persisted, mirrored by the toolbar menus) // 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, // Shared list chrome: view picker + filter chips + filter drawer + tools,
// driven off this page's existing sorts/hiddenKeys state. Same hook every // driven off this page's existing sorts/hiddenKeys state. Same hook every
// other list page uses, so /cron looks and behaves like /domains. // 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) => { const applyExtraViewState = useCallback((state) => {
if (state.filter !== undefined) setFilter(state.filter);
if (state.search !== undefined) setSearch(state.search); if (state.search !== undefined) setSearch(state.search);
}, []); }, []);
@@ -279,24 +282,10 @@ const CronJobs = () => {
const pausedCount = jobs.length - enabledCount; const pausedCount = jobs.length - enabledCount;
const q = search.trim().toLowerCase(); const q = search.trim().toLowerCase();
const shown = jobs.filter((job) => { const shown = jobs.filter((job) => !q
const state = jobState(job); || (job.name || '').toLowerCase().includes(q)
const matchesFilter = filter === 'all' || (job.command || '').toLowerCase().includes(q));
|| (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 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 const serviceNote = status?.available === false
? 'Cron service unavailable' ? 'Cron service unavailable'
@@ -361,6 +350,11 @@ const CronJobs = () => {
key: 'status', key: 'status',
header: 'Status', header: 'Status',
sortable: true, 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, sortValue: (job) => jobState(job).label,
render: (job) => { render: (job) => {
const state = jobState(job); const state = jobState(job);
@@ -477,7 +471,6 @@ const CronJobs = () => {
onCreate={chrome.createView} onCreate={chrome.createView}
/> />
<ListToolbar <ListToolbar
filters={<SegControl value={filter} onChange={setFilter} options={filterOptions} />}
count={serviceNote && <span className="cron-servicenote">{serviceNote}</span>} count={serviceNote && <span className="cron-servicenote">{serviceNote}</span>}
tools={( tools={(
<> <>