feat(mcp): i18n + a11y McpStatusBadge status labels (#2635)

This commit is contained in:
Aashir Athar
2026-05-28 14:44:32 -07:00
committed by GitHub
parent dbe238384a
commit 94f8e4e81c
2 changed files with 59 additions and 9 deletions
@@ -0,0 +1,44 @@
/**
* Tests for McpStatusBadge — renders the i18n'd label and a11y role
* for each ServerStatus, and forwards custom className.
*/
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import McpStatusBadge from './McpStatusBadge';
import type { ServerStatus } from './types';
describe('McpStatusBadge', () => {
it.each<[ServerStatus, string]>([
['connected', 'Connected'],
['connecting', 'Connecting'],
['disconnected', 'Disconnected'],
['error', 'Error'],
])('renders i18n label for status=%s', (status, expectedLabel) => {
render(<McpStatusBadge status={status} />);
expect(screen.getByRole('status')).toHaveTextContent(expectedLabel);
});
it('exposes role="status" and aria-live="polite" for assistive tech', () => {
render(<McpStatusBadge status="connecting" />);
const badge = screen.getByRole('status');
expect(badge).toHaveAttribute('aria-live', 'polite');
});
it('falls back to the disconnected style for an unknown status value', () => {
// ServerStatus is a closed union, but the runtime fallback exists for
// forward-compat with possible future Rust-side variants — exercise it.
render(<McpStatusBadge status={'spawning' as ServerStatus} />);
expect(screen.getByRole('status')).toHaveTextContent('Disconnected');
});
it('appends the optional className without dropping the built-in classes', () => {
render(<McpStatusBadge status="connected" className="ml-2 my-custom-class" />);
const badge = screen.getByRole('status');
expect(badge.className).toContain('my-custom-class');
expect(badge.className).toContain('ml-2');
// Built-in look-and-feel preserved.
expect(badge.className).toContain('rounded-full');
expect(badge.className).toContain('bg-sage-500/10');
});
});
@@ -1,25 +1,28 @@
/**
* Status badge for MCP server connection states.
* Mirrors ChannelStatusBadge but uses ServerStatus values.
* Mirrors ChannelStatusBadge but uses ServerStatus values; reuses the
* shared `channels.status.*` i18n keys since the label vocabulary is
* identical (Connected / Connecting / Disconnected / Error).
*/
import { useT } from '../../../lib/i18n/I18nContext';
import type { ServerStatus } from './types';
const STATUS_STYLES: Record<ServerStatus, { label: string; className: string }> = {
const STATUS_META: Record<ServerStatus, { i18nKey: string; className: string }> = {
connected: {
label: 'Connected',
i18nKey: 'channels.status.connected',
className: 'bg-sage-500/10 text-sage-700 border-sage-500/30 dark:text-sage-300',
},
connecting: {
label: 'Connecting',
i18nKey: 'channels.status.connecting',
className: 'bg-amber-500/10 text-amber-700 border-amber-500/30 dark:text-amber-300',
},
disconnected: {
label: 'Disconnected',
i18nKey: 'channels.status.disconnected',
className:
'bg-stone-100 dark:bg-neutral-800 text-stone-500 dark:text-neutral-400 border-stone-200 dark:border-neutral-700',
},
error: {
label: 'Error',
i18nKey: 'channels.status.error',
className: 'bg-coral-500/10 text-coral-700 border-coral-500/30 dark:text-coral-300',
},
};
@@ -30,11 +33,14 @@ interface McpStatusBadgeProps {
}
const McpStatusBadge = ({ status, className = '' }: McpStatusBadgeProps) => {
const style = STATUS_STYLES[status] ?? STATUS_STYLES.disconnected;
const { t } = useT();
const meta = STATUS_META[status] ?? STATUS_META.disconnected;
return (
<span
className={`shrink-0 px-2 py-1 text-[11px] border rounded-full ${style.className} ${className}`}>
{style.label}
role="status"
aria-live="polite"
className={`shrink-0 px-2 py-1 text-[11px] border rounded-full ${meta.className} ${className}`}>
{t(meta.i18nKey)}
</span>
);
};