Skip to content

feat(console): improve empty states for Jobs, Scheduler, Plugins, and MCP#287

Open
maxnoller wants to merge 4 commits intomainfrom
feat/empty-state-improvements
Open

feat(console): improve empty states for Jobs, Scheduler, Plugins, and MCP#287
maxnoller wants to merge 4 commits intomainfrom
feat/empty-state-improvements

Conversation

@maxnoller
Copy link
Copy Markdown
Member

@maxnoller maxnoller commented Apr 13, 2026

Summary

This PR replaces bare, low-information empty states across four admin console pages with helpful, actionable alternatives that include a description of what the page is for and a clear CTA. A pre-existing crash in channels-catalog.ts was found and fixed during browser verification.


Jobs (/admin/jobs)

  • Before: Five empty kanban columns each showing "No jobs".
  • After: When the board has no jobs at all, the columns are replaced by a single centered empty state:
    • "Jobs are async tasks created by the agent or triggered manually."
    • New Job button (links to /admin/scheduler, same as the existing header button).
    • Individual column "No jobs" placeholders are preserved for when there are jobs but a column happens to be empty.

Scheduler (/admin/scheduler)

  • Before: "No scheduled work yet." with the full job editor always visible on the right.
  • After:
    • Empty list shows "Scheduled jobs let you run agent tasks automatically on a cron schedule." and a New job button.
    • The editor panel on the right is hidden until the user clicks New job (in the header or empty state) or selects an existing job. Clicking Cancel with no job selected hides the editor again.

Plugins (/admin/plugins)

  • Before: "No plugins match this filter." for both "no plugins installed" and "filter has no results".
  • After:
    • When the server reports zero installed plugins: centered empty state with "Plugins extend HybridClaw with custom commands, tools, and hooks." and a Plugin documentation link (hybridclaw.io/docs/extensibility/plugins).
    • When a filter is active and returns no results: existing "No plugins match this filter." message is kept.

MCP (/admin/mcp)

  • Before: Full server editor form always visible even when no servers are configured.
  • After:
    • When no servers exist and the editor is not open: full-page centered empty state with "MCP servers let the agent call external tools over the Model Context Protocol." and an Add your first server button.
    • Clicking the button reveals the editor. Once at least one server exists the normal list + editor two-column layout is used.
    • The New server button in the header is hidden when there are no servers (the empty-state CTA takes its place). It reappears once servers exist.

CSS additions (styles.css)

Two new utility classes following the project's existing conventions:

  • .jobs-board-empty — full-page centered flex layout (used for Jobs board-level and MCP page-level empty states).
  • .empty-state-cta — panel-level centered bordered block with description + CTA button (used inside list panels for Scheduler and MCP).

Bugfix: channels-catalog.ts crash on missing integration keys

During browser testing, the Scheduler page crashed with Cannot read properties of undefined (reading 'enabled') in describeSlack. The describe* functions assumed every integration key (slack, discord, telegram, etc.) is always present in the config object, but they are absent when the integration has never been configured. Fixed all seven functions with optional chaining and nullish coalescing so they return an "available / not configured" state gracefully.


Test plan

  • /admin/jobs — no jobs → centered empty state with "New Job" button; clicking goes to /admin/scheduler. Verified in browser.
  • /admin/scheduler — no jobs → list shows CTA; editor panel is hidden. Clicking "New job" (header or CTA) reveals the editor. Verified in browser.
  • /admin/plugins — no plugins → description + docs link in registry panel. Verified in browser.
  • /admin/mcp — no servers → full-page centered empty state; "Add your first server" opens the editor. Verified in browser.
  • /admin/jobs with some jobs — kanban renders normally; empty columns show the dashed "No jobs" placeholder.
  • /admin/scheduler with existing jobs — list renders, clicking a job opens the editor.
  • /admin/plugins with plugins but an active filter matching nothing — "No plugins match this filter." shown (not the docs CTA).
  • /admin/mcp with servers — normal list + editor layout with "New server" header button.
  • /admin/channels — channel catalog renders without crash on instances where some integrations are absent from config.

… MCP pages

- **Jobs**: show a centered empty state with description and "New Job" CTA
  when the board has no jobs at all, instead of showing five empty columns
- **Scheduler**: replace bare "No scheduled work yet." with a centered CTA
  that includes a description and "New job" button; hide the editor panel
  on the right until the user clicks "New job" or selects an existing job
- **Plugins**: distinguish between "no plugins installed" (shows description
  + documentation link) and "filter matches nothing" (keeps existing message)
- **MCP**: when no servers are configured show a full-page centered empty
  state with description and "Add your first server" CTA instead of
  immediately rendering the editor form; once at least one server exists
  the normal list + editor layout is used
- **CSS**: add `.jobs-board-empty` (full-page centered) and
  `.empty-state-cta` (panel-level centered with border) utility classes
  used by the new empty states
When an integration (slack, discord, telegram, etc.) is not present in
the config object at all, the describe* functions in channels-catalog.ts
would crash with "Cannot read properties of undefined". Use optional
chaining and nullish coalescing throughout so each function safely
returns an "available / not configured" state when its config section is
absent rather than throwing.

Exposed by the scheduler page on instances without Slack configured.
- Rename .jobs-board-empty to .page-empty (was also used on MCP page)
- Extract openNewServer() and openNewJob() callbacks to remove 5 inline
  triple-setter duplications across mcp.tsx and scheduler.tsx
- Fix mcp.tsx empty-state condition: guard with !mcpQuery.isError so a
  fetch error no longer silently shows the "Add your first server" CTA
- Deduplicate URLSearchParams parse in scheduler.tsx useState initializers
@maxnoller
Copy link
Copy Markdown
Member Author

Code review

Found 3 issues:

  1. describeDiscord: enabled is always true when config.discord is absent — the ?? false fallback is dead code. config.discord?.groupPolicy !== 'disabled' evaluates undefined !== 'disabled', which is true, so the || yields true before ?? can fire. When Discord has no config section, the channel shows as configured (not available) and active is gated only on tokenConfigured. Fix: guard with config.discord ? (...) : false.

const guildCount = countDiscordGuilds(config);
const overrideCount = countDiscordOverrides(config);
const enabled =
(config.discord?.commandsOnly ||
config.discord?.groupPolicy !== 'disabled') ??
false;
const tokenConfigured = options.discordTokenConfigured === true;
const active = enabled && tokenConfigured;
const configured =

  1. describeWhatsApp: same !== 'disabled' flaw with no guard — when config.whatsapp is absent but options.whatsappLinked === true, enabled evaluates to true (both undefined !== 'disabled' comparisons are true) and statusTone becomes 'active'. The summary line would render "Linked device · groups undefined".

const linked = options.whatsappLinked === true;
const enabled =
config.whatsapp?.dmPolicy !== 'disabled' ||
config.whatsapp?.groupPolicy !== 'disabled';
const summary = linked
? enabled
? `Linked device · groups ${config.whatsapp?.groupPolicy}`
: 'Linked device available but transport is off'
: enabled
? 'Link device to enable WhatsApp'
: 'WhatsApp transport is off';
const statusTone = linked ? (enabled ? 'active' : 'configured') : 'available';

  1. mcp.tsx: showEditor not reset on server deletedeleteMutation.onSuccess calls setSelectedName(null) and setDraft(createDraft()) but never setShowEditor(false). After deleting the last server, showEditor remains true, the full-page empty state condition (!showEditor) is never satisfied, and the UI shows a hollow two-column grid with a blank editor instead of the intended "Add your first server" prompt.

},
onError: (error) => {
toast.error('Save failed', getErrorMessage(error));
},
});
const deleteMutation = useMutation({
mutationFn: () => deleteMcpServer(auth.token, draft.name.trim()),
onSuccess: (payload) => {
queryClient.setQueryData(['mcp', auth.token], payload);
setSelectedName(null);
setDraft(createDraft());

🤖 Generated with Claude Code

If this code review was useful, please react with 👍. Otherwise, react with 👎.

- describeDiscord: replace ineffective `?? false` with explicit `config.discord ? ... : false`
  guard so enabled=false (not true) when discord config section is absent
- describeWhatsApp: same fix; without it a linked-but-unconfigured device
  showed as active with "groups undefined" in the summary
- mcp.tsx: call setShowEditor(false) in deleteMutation.onSuccess so deleting
  the last server collapses back to the full-page empty state
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant