Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 55 additions & 52 deletions apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
'use client'

import { useEffect, useRef, useState } from 'react'
import { Badge, Button, Chip, ChipConfirmModal, cn, Tooltip, toast } from '@sim/emcn'
import {
Badge,
Chip,
ChipConfirmModal,
CollapsibleCard,
cn,
OverflowText,
Tooltip,
toast,
} from '@sim/emcn'
import { ArrowLeft, ChevronDown, Plus } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
Expand Down Expand Up @@ -535,59 +544,53 @@ export function MCP() {
const requiredParams = tool.inputSchema?.required || []

return (
<div
<CollapsibleCard
key={tool.name}
className='overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-3)]'
>
<Button
type='button'
variant='ghost'
onClick={() => hasParams && toggleToolExpanded(tool.name)}
className={cn(
'flex h-auto w-full items-start justify-between rounded-none px-2.5 py-2 text-left text-sm',
hasParams && 'cursor-pointer hover-hover:bg-[var(--surface-4)]'
)}
disabled={!hasParams}
>
<div className='flex-1'>
<div className='flex h-[16px] items-center gap-1.5'>
<p className='text-[var(--text-primary)] text-sm leading-none'>
{tool.name}
</p>
{issues.length > 0 && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<div className='flex items-center'>
<Badge variant={getIssueBadgeVariant(issues[0].issue)} size='sm'>
{getIssueBadgeLabel(issues[0].issue)}
</Badge>
</div>
</Tooltip.Trigger>
<Tooltip.Content>
Update in: {affectedWorkflows.join(', ')}
</Tooltip.Content>
</Tooltip.Root>
)}
</div>
collapsed={!isExpanded || !hasParams}
disabled={!hasParams}
onToggleCollapse={() => toggleToolExpanded(tool.name)}
title={
<>
<OverflowText
label={tool.name}
className='text-[var(--text-primary)]'
focusTarget='nearest-interactive'
/>
{tool.description && (
<p className='mt-1 text-[var(--text-tertiary)] text-sm'>
{tool.description}
</p>
<span className='mt-1 block whitespace-normal'>{tool.description}</span>
)}
</div>
{hasParams && (
<ChevronDown
className={cn(
'mt-0.5 size-[14px] shrink-0 text-[var(--text-muted)] transition-transform duration-200',
isExpanded && 'rotate-180'
)}
/>
)}
</Button>

</>
}
badge={
<>
{issues.length > 0 && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='flex shrink-0 items-center'>
<Badge variant={getIssueBadgeVariant(issues[0].issue)} size='sm'>
{getIssueBadgeLabel(issues[0].issue)}
</Badge>
</span>
</Tooltip.Trigger>
<Tooltip.Content>
Update in: {affectedWorkflows.join(', ')}
</Tooltip.Content>
</Tooltip.Root>
)}
{hasParams && (
<ChevronDown
className={cn(
'size-[14px] shrink-0 text-[var(--text-muted)] transition-transform duration-200',
isExpanded && 'rotate-180'
)}
/>
)}
</>
}
>
{isExpanded && hasParams && (
<div className='border-[var(--border-1)] border-t bg-[var(--surface-2)] px-2.5 py-2'>
<p className='mb-1.5 text-[var(--text-muted)] text-caption uppercase tracking-wide'>
<>
<p className='text-[var(--text-muted)] text-caption uppercase tracking-wide'>
Parameters
</p>
<div className='flex flex-col gap-1.5'>
Expand Down Expand Up @@ -631,9 +634,9 @@ export function MCP() {
}
)}
</div>
</div>
</>
)}
</div>
</CollapsibleCard>
)
})}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,38 @@ describe('CollapsibleCard', () => {
}
)

it('prevents disabled header activation without disabling independent actions', () => {
const toggle = vi.fn()
const action = vi.fn()
mount(
<CollapsibleCard
title='Status'
disabled
collapsed
onToggleCollapse={toggle}
actions={
<button type='button' onClick={action}>
Refresh
</button>
}
>
Content
</CollapsibleCard>
)
const trigger = container!.querySelector<HTMLElement>('[role="button"]')!
expect(trigger.tabIndex).toBe(-1)
expect(trigger.getAttribute('aria-disabled')).toBe('true')
act(() => {
trigger.click()
trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
trigger.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }))
container!.querySelector<HTMLButtonElement>('button')!.click()
})
expect(toggle).not.toHaveBeenCalled()
expect(action).toHaveBeenCalledTimes(1)
expect(container!.textContent).not.toContain('Content')
})

it('keeps enabled and disabled actions outside the collapse target', () => {
const toggle = vi.fn()
const add = vi.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface CollapsibleCardProps
badge?: React.ReactNode
/** Header actions, outside the collapse target and arranged with standard spacing. */
actions?: React.ReactNode
/** Prevent header activation and remove it from tab order; independent actions stay available. */
disabled?: boolean
collapsed: boolean
onToggleCollapse: () => void
/** Animate expansion using the shared Expandable height transition. */
Expand Down Expand Up @@ -42,6 +44,7 @@ export function CollapsibleCard({
badge,
actions,
collapsed,
disabled = false,
onToggleCollapse,
animated = false,
contentProps,
Expand All @@ -58,16 +61,18 @@ export function CollapsibleCard({
<>
<div
role='button'
tabIndex={0}
tabIndex={disabled ? -1 : 0}
aria-expanded={!collapsed}
aria-disabled={disabled || undefined}
aria-controls={contentProps?.id}
className={cn(
'flex min-w-0 flex-1 cursor-pointer items-center gap-2 px-2.5 py-[5px]',
'flex min-w-0 flex-1 items-center gap-2 px-2.5 py-[5px]',
!disabled && 'cursor-pointer',
actions && 'pr-2'
)}
onClick={onToggleCollapse}
onClick={disabled ? undefined : onToggleCollapse}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return
if (disabled || event.target !== event.currentTarget) return
handleKeyboardActivation(event, onToggleCollapse)
}}
>
Expand Down
Loading