refactor: dry out renderIcon to renderMCPIcon reusable component

This commit is contained in:
Dustin Healy 2026-01-05 22:42:57 -08:00 committed by Danny Avila
parent 3c142b64e9
commit c8dffd3f07
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
6 changed files with 98 additions and 101 deletions

View file

@ -1,16 +1,15 @@
import * as Ariakit from '@ariakit/react';
import { Check } from 'lucide-react';
import { MCPIcon } from '@librechat/client';
import type { MCPServerDefinition } from '~/hooks/MCP/useMCPServerManager';
import type { MCPServerStatusIconProps } from './MCPServerStatusIcon';
import MCPServerStatusIcon from './MCPServerStatusIcon';
import ClickHouseIcon from './ClickHouseIcon';
import {
getStatusColor,
getStatusTextKey,
shouldShowActionButton,
type ConnectionStatusMap,
} from './mcpServerUtils';
import { renderMCPIcon } from './renderMCPIcon';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -41,28 +40,6 @@ export default function MCPServerMenuItem({
// Include status in aria-label so screen readers announce it
const accessibleLabel = `${displayName}, ${statusText}`;
const renderIcon = () => {
if (server.config?.iconPath) {
return (
<img
src={server.config.iconPath}
className="h-8 w-8 rounded-lg object-cover"
alt={displayName}
/>
);
}
if (server.serverName.toLowerCase().includes('clickhouse')) {
return <ClickHouseIcon className="h-8 w-8 rounded-lg object-cover" alt={displayName} />;
}
return (
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-surface-tertiary">
<MCPIcon className="h-5 w-5 text-text-secondary" />
</div>
);
};
return (
<Ariakit.MenuItemCheckbox
hideOnClick={false}
@ -81,7 +58,13 @@ export default function MCPServerMenuItem({
>
{/* Server Icon with Status Dot */}
<div className="relative flex-shrink-0">
{renderIcon()}
{renderMCPIcon({
iconPath: server.config?.iconPath,
serverName: server.serverName,
displayName,
className: 'h-8 w-8 rounded-lg object-cover',
wrapDefault: true,
})}
{/* Status dot - decorative, status is announced via aria-label on MenuItem */}
<div
aria-hidden="true"

View file

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { MCPIcon } from '@librechat/client';
import type { MCPServerDefinition } from '~/hooks/MCP/useMCPServerManager';
import { getSelectedServerIcons } from './mcpServerUtils';
import ClickHouseIcon from './ClickHouseIcon';
import { renderMCPIcon } from './renderMCPIcon';
import { cn } from '~/utils';
interface StackedMCPIconsProps {
@ -59,29 +59,6 @@ export default function StackedMCPIcons({
const sizes = sizeConfig[iconSize];
const colors = variantConfig[variant];
const renderIcon = (icon: { iconPath: string | null; displayName: string }) => {
if (icon.iconPath === 'clickhouse') {
return (
<ClickHouseIcon
className={cn('rounded-full object-cover', sizes.icon)}
alt={icon.displayName}
/>
);
}
if (icon.iconPath) {
return (
<img
src={icon.iconPath}
alt={icon.displayName}
className={cn('rounded-full object-cover', sizes.icon)}
/>
);
}
return <MCPIcon className={cn('text-text-primary', sizes.icon)} />;
};
return (
<div className="flex items-center">
{icons.map((icon, index) => (
@ -97,7 +74,12 @@ export default function StackedMCPIcons({
)}
style={{ zIndex: icons.length - index }}
>
{renderIcon(icon)}
{renderMCPIcon({
iconPath: icon.iconPath,
serverName: icon.serverName,
displayName: icon.displayName,
className: cn('rounded-full object-cover', sizes.icon),
})}
</div>
))}
{overflowCount > 0 && (

View file

@ -40,7 +40,7 @@ export function getSelectedServerIcons(
clickhouseServers.push({
key: server.serverName,
serverName: server.serverName,
iconPath: 'clickhouse',
iconPath: null,
displayName,
});
} else {

View file

@ -0,0 +1,60 @@
import type { ReactNode } from 'react';
import { MCPIcon } from '@librechat/client';
import ClickHouseIcon from './ClickHouseIcon';
import { cn } from '~/utils';
export interface RenderMCPIconOptions {
iconPath?: string | null;
serverName?: string;
name?: string;
displayName?: string;
className?: string;
alt?: string;
fallbackIcon?: ReactNode;
wrapDefault?: boolean;
}
export function renderMCPIcon({
iconPath,
serverName,
name,
displayName,
className = 'h-8 w-8 rounded-lg object-cover',
alt,
fallbackIcon,
wrapDefault = false,
}: RenderMCPIconOptions): ReactNode {
const altText = alt || displayName || name || serverName || '';
const isClickHouse = serverName?.toLowerCase().includes('clickhouse') ?? false;
if (iconPath) {
return <img src={iconPath} className={className} alt={altText} />;
}
if (isClickHouse) {
return <ClickHouseIcon className={className} alt={altText} />;
}
if (fallbackIcon) {
return fallbackIcon;
}
const defaultIcon = <MCPIcon className={cn('text-text-secondary', className)} />;
if (wrapDefault) {
return (
<div
className={cn('flex items-center justify-center rounded-lg bg-surface-tertiary', className)}
>
<MCPIcon
className={cn(
'h-5 w-5 text-text-secondary',
className.includes('size-8') ? 'size-5' : '',
)}
/>
</div>
);
}
return defaultIcon;
}

View file

@ -1,8 +1,7 @@
import { useState, useRef } from 'react';
import { MCPIcon } from '@librechat/client';
import { PermissionBits, hasPermissions } from 'librechat-data-provider';
import type { MCPServerStatusIconProps } from '~/components/MCP/MCPServerStatusIcon';
import ClickHouseIcon from '~/components/MCP/ClickHouseIcon';
import { renderMCPIcon } from '~/components/MCP/renderMCPIcon';
import type { MCPServerDefinition } from '~/hooks';
import { getStatusDotColor } from './MCPStatusBadge';
import MCPServerDialog from './MCPServerDialog';
@ -71,29 +70,6 @@ export default function MCPServerCard({
setDialogOpen(true);
};
const renderIcon = () => {
if (server.config?.iconPath) {
return (
<img
src={server.config.iconPath}
className="size-8 rounded-lg object-cover"
alt=""
aria-hidden="true"
/>
);
}
if (server.serverName.toLowerCase().includes('clickhouse')) {
return <ClickHouseIcon className="size-8 rounded-lg object-cover" alt="" />;
}
return (
<div className="flex size-8 items-center justify-center rounded-lg bg-surface-tertiary">
<MCPIcon className="size-5 text-text-secondary" aria-hidden="true" />
</div>
);
};
// Determine status text for accessibility
const getStatusText = () => {
if (isInitializing) return localize('com_nav_mcp_status_initializing');
@ -121,7 +97,14 @@ export default function MCPServerCard({
>
{/* Server Icon with Status Dot */}
<div className="relative flex-shrink-0">
{renderIcon()}
{renderMCPIcon({
iconPath: server.config?.iconPath,
serverName: server.serverName,
displayName,
className: 'size-8 rounded-lg object-cover',
alt: '',
wrapDefault: true,
})}
{/* Status dot - color indicates connection state */}
<div
className={cn(

View file

@ -1,6 +1,6 @@
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
import type { AgentToolType } from 'librechat-data-provider';
import ClickHouseIcon from '~/components/MCP/ClickHouseIcon';
import { renderMCPIcon } from '~/components/MCP/renderMCPIcon';
import { useLocalize } from '~/hooks';
type MCPToolItemProps = {
@ -72,34 +72,23 @@ function MCPToolItem({
const buttonState = getButtonState();
const renderIcon = () => {
if (icon) {
return (
<img
src={icon}
alt={localize('com_ui_logo', { 0: name })}
className="h-full w-full rounded-[5px] bg-white"
/>
);
}
if (name.toLowerCase().includes('clickhouse')) {
return <ClickHouseIcon className="h-full w-full rounded-[5px] object-cover" alt={name} />;
}
return (
<div className="flex h-full w-full items-center justify-center rounded-[5px] border border-border-medium bg-transparent">
<Wrench className="h-8 w-8 text-text-secondary" />
</div>
);
};
return (
<div className="flex flex-col gap-4 rounded border border-border-medium bg-transparent p-6">
<div className="flex gap-4">
<div className="h-[70px] w-[70px] shrink-0">
<div className="relative h-full w-full">
{renderIcon()}
{renderMCPIcon({
iconPath: icon,
serverName: name,
displayName: name,
className: 'h-full w-full rounded-[5px] object-cover',
alt: localize('com_ui_logo', { 0: name }),
fallbackIcon: (
<div className="flex h-full w-full items-center justify-center rounded-[5px] border border-border-medium bg-transparent">
<Wrench className="h-8 w-8 text-text-secondary" />
</div>
),
})}
<div className="absolute inset-0 rounded-[5px] ring-1 ring-inset ring-black/10"></div>
</div>
</div>