mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix: modals, settings, panel (#2187)
This commit is contained in:
@@ -219,6 +219,7 @@ export function Chat() {
|
||||
const [chatMessage, setChatMessage] = useState('')
|
||||
const [promptHistory, setPromptHistory] = useState<string[]>([])
|
||||
const [historyIndex, setHistoryIndex] = useState(-1)
|
||||
const [moreMenuOpen, setMoreMenuOpen] = useState(false)
|
||||
|
||||
// Refs
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -836,7 +837,7 @@ export function Chat() {
|
||||
|
||||
<div className='flex flex-shrink-0 items-center gap-[8px]'>
|
||||
{/* More menu with actions */}
|
||||
<Popover variant='default'>
|
||||
<Popover variant='default' open={moreMenuOpen} onOpenChange={setMoreMenuOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -858,6 +859,7 @@ export function Chat() {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (activeWorkflowId) exportChatCSV(activeWorkflowId)
|
||||
setMoreMenuOpen(false)
|
||||
}}
|
||||
disabled={workflowMessages.length === 0}
|
||||
>
|
||||
@@ -868,6 +870,7 @@ export function Chat() {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (activeWorkflowId) clearChat(activeWorkflowId)
|
||||
setMoreMenuOpen(false)
|
||||
}}
|
||||
disabled={workflowMessages.length === 0}
|
||||
>
|
||||
|
||||
@@ -780,7 +780,7 @@ function AuthSelector({
|
||||
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{authType === 'email' ? 'Allowed emails' : 'Allowed SSO emails'}
|
||||
</Label>
|
||||
<div className='scrollbar-hide flex max-h-32 min-h-9 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[4px] focus-within:outline-none dark:bg-[var(--surface-9)]'>
|
||||
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-9)]'>
|
||||
{invalidEmails.map((email, index) => (
|
||||
<EmailTag
|
||||
key={`invalid-${index}`}
|
||||
@@ -798,7 +798,7 @@ function AuthSelector({
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
<Input
|
||||
<input
|
||||
type='text'
|
||||
value={emailInputValue}
|
||||
onChange={(e) => setEmailInputValue(e.target.value)}
|
||||
@@ -810,10 +810,7 @@ function AuthSelector({
|
||||
? 'Add another email'
|
||||
: 'Enter emails or domains (@example.com)'
|
||||
}
|
||||
className={cn(
|
||||
'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 text-[13px] focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
emails.length > 0 || invalidEmails.length > 0 ? 'pl-[4px]' : 'pl-[4px]'
|
||||
)}
|
||||
className='min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-foreground text-sm outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Loader2, Plus } from 'lucide-react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Button, Combobox, Input, Label, Textarea } from '@/components/emcn'
|
||||
import {
|
||||
Modal,
|
||||
@@ -118,7 +118,7 @@ export function TemplateDeploy({
|
||||
|
||||
setLoadingCreators(true)
|
||||
try {
|
||||
const response = await fetch('/api/creator-profiles')
|
||||
const response = await fetch('/api/creators')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
const profiles = (data.profiles || []).map((profile: any) => ({
|
||||
@@ -321,24 +321,23 @@ export function TemplateDeploy({
|
||||
{creatorOptions.length === 0 && !loadingCreators ? (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
variant='primary'
|
||||
onClick={() => {
|
||||
try {
|
||||
const event = new CustomEvent('open-settings', {
|
||||
detail: { tab: 'creator-profile' },
|
||||
detail: { tab: 'template-profile' },
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
logger.info('Opened Settings modal at creator-profile section')
|
||||
logger.info('Opened Settings modal at template-profile section')
|
||||
} catch (error) {
|
||||
logger.error('Failed to open Settings modal for creator profile', {
|
||||
logger.error('Failed to open Settings modal for template profile', {
|
||||
error,
|
||||
})
|
||||
}
|
||||
}}
|
||||
className='gap-[8px]'
|
||||
>
|
||||
<Plus className='h-[14px] w-[14px] text-[var(--text-muted)]' />
|
||||
<span className='text-[var(--text-muted)]'>Create a Creator Profile</span>
|
||||
<span>Create Template Profile</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Combobox
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Check } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
@@ -339,63 +331,63 @@ export function OAuthRequiredModal({
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<ModalContent className='sm:max-w-md'>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Additional Access Required</ModalTitle>
|
||||
<ModalDescription>
|
||||
The "{toolName}" tool requires access to your {providerName} account to function
|
||||
properly.
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
<div className='flex flex-col gap-4 py-4'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className='rounded-full bg-muted p-2'>
|
||||
<ProviderIcon className='h-5 w-5' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium text-sm'>Connect {providerName}</p>
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
You need to connect your {providerName} account to continue
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{displayScopes.length > 0 && (
|
||||
<div className='rounded-md border bg-muted/50'>
|
||||
<div className='border-b px-4 py-3'>
|
||||
<h4 className='font-medium text-sm'>Permissions requested</h4>
|
||||
<ModalContent className='w-[460px]'>
|
||||
<ModalHeader>Connect {providerName}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
<div className='flex items-center gap-[14px]'>
|
||||
<div className='flex h-[40px] w-[40px] flex-shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-6)]'>
|
||||
<ProviderIcon className='h-[18px] w-[18px]' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<p className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Connect your {providerName} account
|
||||
</p>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
The "{toolName}" tool requires access to your account
|
||||
</p>
|
||||
</div>
|
||||
<ul className='max-h-[400px] space-y-3 overflow-y-auto px-4 py-3'>
|
||||
{displayScopes.map((scope) => (
|
||||
<li key={scope} className='flex items-start gap-2 text-sm'>
|
||||
<div className='mt-1 rounded-full bg-muted p-0.5'>
|
||||
<Check className='h-3 w-3' />
|
||||
</div>
|
||||
<div className='text-muted-foreground'>
|
||||
<span>{getScopeDescription(scope)}</span>
|
||||
{newScopesSet.has(scope) && (
|
||||
<span className='ml-2 rounded-[4px] border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300'>
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{displayScopes.length > 0 && (
|
||||
<div className='rounded-[8px] border bg-[var(--surface-6)]'>
|
||||
<div className='border-b px-[14px] py-[10px]'>
|
||||
<h4 className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Permissions requested
|
||||
</h4>
|
||||
</div>
|
||||
<ul className='max-h-[330px] space-y-[10px] overflow-y-auto px-[14px] py-[12px]'>
|
||||
{displayScopes.map((scope) => (
|
||||
<li key={scope} className='flex items-start gap-[10px]'>
|
||||
<div className='mt-[3px] flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
|
||||
<Check className='h-[10px] w-[10px] text-[var(--text-primary)]' />
|
||||
</div>
|
||||
<div className='flex-1 text-[12px] text-[var(--text-primary)]'>
|
||||
<span>{getScopeDescription(scope)}</span>
|
||||
{newScopesSet.has(scope) && (
|
||||
<span className='ml-[8px] rounded-[4px] border border-amber-500/30 bg-amber-500/10 px-[6px] py-[2px] text-[10px] text-amber-300'>
|
||||
New
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='outline' onClick={onClose} className='h-[32px] px-[12px]'>
|
||||
<Button variant='active' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
type='button'
|
||||
onClick={handleConnectDirectly}
|
||||
className='h-[32px] px-[12px]'
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
>
|
||||
Connect Now
|
||||
Connect
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -166,18 +166,6 @@ export function Panel() {
|
||||
setHasHydrated(true)
|
||||
}, [setHasHydrated])
|
||||
|
||||
/**
|
||||
* Focus Copilot user input when the Copilot tab becomes active or when
|
||||
* the panel loads with Copilot already selected, after hydration.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!_hasHydrated || activeTab !== 'copilot') {
|
||||
return
|
||||
}
|
||||
|
||||
copilotRef.current?.focusInput()
|
||||
}, [_hasHydrated, activeTab])
|
||||
|
||||
/**
|
||||
* Handles tab click events
|
||||
*/
|
||||
|
||||
@@ -241,13 +241,13 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
|
||||
{isLoading ? (
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[70px]' />
|
||||
<Skeleton className='h-5 w-[70px]' />
|
||||
<div className='text-[13px] text-[var(--text-muted)]'>
|
||||
<Skeleton className='h-[13px] w-[140px]' />
|
||||
<Skeleton className='h-5 w-[140px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[55px]' />
|
||||
<Skeleton className='h-5 w-[55px]' />
|
||||
<ApiKeySkeleton />
|
||||
<ApiKeySkeleton />
|
||||
</div>
|
||||
@@ -624,10 +624,10 @@ function ApiKeySkeleton() {
|
||||
<div className='flex items-center justify-between gap-[12px]'>
|
||||
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[14px] w-[80px]' />
|
||||
<Skeleton className='h-[13px] w-[140px]' />
|
||||
<Skeleton className='h-5 w-[80px]' />
|
||||
<Skeleton className='h-5 w-[140px]' />
|
||||
</div>
|
||||
<Skeleton className='h-[13px] w-[100px]' />
|
||||
<Skeleton className='h-5 w-[100px]' />
|
||||
</div>
|
||||
<Skeleton className='h-[26px] w-[48px] rounded-[6px]' />
|
||||
</div>
|
||||
|
||||
@@ -752,13 +752,13 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[70px]' />
|
||||
<Skeleton className='h-5 w-[70px]' />
|
||||
<div className='text-[13px] text-[var(--text-muted)]'>
|
||||
<Skeleton className='h-[13px] w-[160px]' />
|
||||
<Skeleton className='h-5 w-[160px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[55px]' />
|
||||
<Skeleton className='h-5 w-[55px]' />
|
||||
{Array.from({ length: 2 }, (_, i) => (
|
||||
<div key={`personal-${i}`} className={GRID_COLS}>
|
||||
<Skeleton className='h-9 rounded-[6px]' />
|
||||
|
||||
@@ -328,21 +328,28 @@ export function General({ onOpenChange }: GeneralProps) {
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
{isEditingName ? (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
className='w-auto border-0 bg-transparent p-0 font-medium text-[14px] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
size={Math.max(name.length, 1)}
|
||||
maxLength={100}
|
||||
disabled={updateProfile.isPending}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
<div className='relative inline-flex'>
|
||||
<span
|
||||
className='invisible whitespace-pre font-medium text-[14px]'
|
||||
aria-hidden='true'
|
||||
>
|
||||
{name || '\u00A0'}
|
||||
</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
className='absolute top-0 left-0 h-full w-full border-0 bg-transparent p-0 font-medium text-[14px] outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
maxLength={100}
|
||||
disabled={updateProfile.isPending}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='h-[12px] w-[12px] flex-shrink-0 p-0'
|
||||
@@ -367,7 +374,7 @@ export function General({ onOpenChange }: GeneralProps) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className='text-[13px] text-[var(--text-muted)]'>{profile?.email || ''}</p>
|
||||
<p className='text-[13px] text-[var(--text-tertiary)]'>{profile?.email || ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
{uploadError && <p className='text-[13px] text-[var(--text-error)]'>{uploadError}</p>}
|
||||
@@ -507,42 +514,34 @@ function GeneralSkeleton() {
|
||||
<Skeleton className='h-9 w-9 rounded-full' />
|
||||
<div className='flex flex-1 flex-col justify-center gap-[1px]'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-24' />
|
||||
<Skeleton className='h-[11px] w-[11px]' />
|
||||
<Skeleton className='h-5 w-24' />
|
||||
<Skeleton className='h-[10.5px] w-[10.5px]' />
|
||||
</div>
|
||||
<Skeleton className='h-[13px] w-40' />
|
||||
<Skeleton className='h-5 w-40' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Theme row - temporarily hidden while light mode is disabled */}
|
||||
{/* <div className='flex items-center justify-between border-b pb-[12px]'>
|
||||
<Skeleton className='h-4 w-12' />
|
||||
<Skeleton className='h-8 w-[100px] rounded-[8px]' />
|
||||
</div> */}
|
||||
|
||||
{/* Auto-connect row */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center justify-between pt-[12px]'>
|
||||
<Skeleton className='h-4 w-36' />
|
||||
<Skeleton className='h-[17px] w-[30px] rounded-[20px]' />
|
||||
<Skeleton className='h-[17px] w-[30px] rounded-full' />
|
||||
</div>
|
||||
|
||||
{/* Error notifications row */}
|
||||
<div className='flex items-center justify-between'>
|
||||
<Skeleton className='h-4 w-36' />
|
||||
<Skeleton className='h-[17px] w-[30px] rounded-[20px]' />
|
||||
<Skeleton className='h-4 w-40' />
|
||||
<Skeleton className='h-[17px] w-[30px] rounded-full' />
|
||||
</div>
|
||||
|
||||
{/* Telemetry row */}
|
||||
<div className='flex items-center justify-between border-t pt-[12px]'>
|
||||
<Skeleton className='h-4 w-40' />
|
||||
<Skeleton className='h-[17px] w-[30px] rounded-[20px]' />
|
||||
<Skeleton className='h-4 w-44' />
|
||||
<Skeleton className='h-[17px] w-[30px] rounded-full' />
|
||||
</div>
|
||||
|
||||
{/* Telemetry description */}
|
||||
<div className='space-y-1'>
|
||||
<Skeleton className='h-3 w-full' />
|
||||
<Skeleton className='h-3 w-4/5' />
|
||||
</div>
|
||||
<Skeleton className='h-[12px] w-full' />
|
||||
<Skeleton className='-mt-2 h-[12px] w-4/5' />
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className='mt-auto flex items-center gap-[8px]'>
|
||||
|
||||
@@ -313,7 +313,7 @@ export function Integrations({ onOpenChange, registerCloseHandler }: Integration
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
{Object.entries(filteredGroupedServices).map(([providerKey, providerServices]) => (
|
||||
<div key={providerKey} className='flex flex-col gap-[8px]'>
|
||||
<Label className='text-[12px] text-[var(--text-muted)]'>
|
||||
<Label className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
{OAUTH_PROVIDERS[providerKey]?.name || 'Other Services'}
|
||||
</Label>
|
||||
{providerServices.map((service) => (
|
||||
|
||||
@@ -2,7 +2,7 @@ export { FormField } from './form-field/form-field'
|
||||
export { FormattedInput } from './formatted-input/formatted-input'
|
||||
export { HeaderRow } from './header-row/header-row'
|
||||
export { McpServerSkeleton } from './mcp-server-skeleton/mcp-server-skeleton'
|
||||
export { ServerListItem } from './server-list-item/server-list-item'
|
||||
export { formatTransportLabel, ServerListItem } from './server-list-item/server-list-item'
|
||||
export type {
|
||||
EnvVarDropdownConfig,
|
||||
HeaderEntry,
|
||||
|
||||
@@ -14,7 +14,10 @@ export function McpServerSkeleton() {
|
||||
</div>
|
||||
<Skeleton className='h-[13px] w-[120px]' />
|
||||
</div>
|
||||
<Skeleton className='h-[30px] w-[54px] flex-shrink-0 rounded-[4px]' />
|
||||
<div className='flex flex-shrink-0 items-center gap-[4px]'>
|
||||
<Skeleton className='h-[30px] w-[60px] rounded-[4px]' />
|
||||
<Skeleton className='h-[30px] w-[54px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button } from '@/components/emcn'
|
||||
/**
|
||||
* Formats transport type for display (e.g., "streamable-http" -> "Streamable-HTTP").
|
||||
*/
|
||||
function formatTransportLabel(transport: string): string {
|
||||
export function formatTransportLabel(transport: string): string {
|
||||
return transport
|
||||
.split('-')
|
||||
.map((word) =>
|
||||
@@ -29,9 +29,19 @@ interface ServerListItemProps {
|
||||
tools: any[]
|
||||
isDeleting: boolean
|
||||
onRemove: () => void
|
||||
onViewDetails: () => void
|
||||
}
|
||||
|
||||
export function ServerListItem({ server, tools, isDeleting, onRemove }: ServerListItemProps) {
|
||||
/**
|
||||
* Renders a single MCP server list item with details and delete actions.
|
||||
*/
|
||||
export function ServerListItem({
|
||||
server,
|
||||
tools,
|
||||
isDeleting,
|
||||
onRemove,
|
||||
onViewDetails,
|
||||
}: ServerListItemProps) {
|
||||
const transportLabel = formatTransportLabel(server.transport || 'http')
|
||||
const toolsLabel = formatToolsLabel(tools)
|
||||
|
||||
@@ -46,9 +56,18 @@ export function ServerListItem({ server, tools, isDeleting, onRemove }: ServerLi
|
||||
</div>
|
||||
<p className='truncate text-[13px] text-[var(--text-muted)]'>{toolsLabel}</p>
|
||||
</div>
|
||||
<Button variant='ghost' className='flex-shrink-0' onClick={onRemove} disabled={isDeleting}>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
<div className='flex flex-shrink-0 items-center gap-[4px]'>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={onViewDetails}
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
>
|
||||
Details
|
||||
</Button>
|
||||
<Button variant='ghost' onClick={onRemove} disabled={isDeleting}>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,11 +25,25 @@ import type { InputFieldType, McpServerFormData, McpServerTestResult } from './c
|
||||
import {
|
||||
FormattedInput,
|
||||
FormField,
|
||||
formatTransportLabel,
|
||||
HeaderRow,
|
||||
McpServerSkeleton,
|
||||
ServerListItem,
|
||||
} from './components'
|
||||
|
||||
interface McpTool {
|
||||
name: string
|
||||
description?: string
|
||||
serverId: string
|
||||
}
|
||||
|
||||
interface McpServer {
|
||||
id: string
|
||||
name?: string
|
||||
transport?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
const logger = createLogger('McpSettings')
|
||||
|
||||
const DEFAULT_FORM_DATA: McpServerFormData = {
|
||||
@@ -86,6 +100,9 @@ export function MCP() {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [serverToDelete, setServerToDelete] = useState<{ id: string; name: string } | null>(null)
|
||||
|
||||
// Server details view state
|
||||
const [selectedServerId, setSelectedServerId] = useState<string | null>(null)
|
||||
|
||||
// Environment variable dropdown state
|
||||
const [showEnvVars, setShowEnvVars] = useState(false)
|
||||
const [envSearchTerm, setEnvSearchTerm] = useState('')
|
||||
@@ -359,6 +376,31 @@ export function MCP() {
|
||||
setShowAddForm(false)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Opens the detail view for a specific server.
|
||||
*/
|
||||
const handleViewDetails = useCallback((serverId: string) => {
|
||||
setSelectedServerId(serverId)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Closes the detail view and returns to the server list.
|
||||
*/
|
||||
const handleBackToList = useCallback(() => {
|
||||
setSelectedServerId(null)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Gets the selected server and its tools for the detail view.
|
||||
*/
|
||||
const selectedServer = useMemo(() => {
|
||||
if (!selectedServerId) return null
|
||||
const server = servers.find((s) => s.id === selectedServerId) as McpServer | undefined
|
||||
if (!server) return null
|
||||
const serverTools = (toolsByServer[selectedServerId] || []) as McpTool[]
|
||||
return { server, tools: serverTools }
|
||||
}, [selectedServerId, servers, toolsByServer])
|
||||
|
||||
const error = toolsError || serversError
|
||||
const hasServers = servers && servers.length > 0
|
||||
const showEmptyState = !hasServers && !showAddForm
|
||||
@@ -369,6 +411,80 @@ export function MCP() {
|
||||
const isSubmitDisabled = serversLoading || isAddingServer || !isFormValid
|
||||
const testButtonLabel = getTestButtonLabel(testResult, isTestingConnection)
|
||||
|
||||
// Show detail view if a server is selected
|
||||
if (selectedServer) {
|
||||
const { server, tools } = selectedServer
|
||||
const transportLabel = formatTransportLabel(server.transport || 'http')
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-[16px]'>
|
||||
<div className='min-h-0 flex-1 overflow-y-auto'>
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Server Name
|
||||
</span>
|
||||
<p className='text-[14px] text-[var(--text-secondary)]'>
|
||||
{server.name || 'Unnamed Server'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-primary)]'>Transport</span>
|
||||
<p className='text-[14px] text-[var(--text-secondary)]'>{transportLabel}</p>
|
||||
</div>
|
||||
|
||||
{server.url && (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-primary)]'>URL</span>
|
||||
<p className='break-all font-mono text-[13px] text-[var(--text-secondary)]'>
|
||||
{server.url}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Tools ({tools.length})
|
||||
</span>
|
||||
{tools.length === 0 ? (
|
||||
<p className='text-[13px] text-[var(--text-muted)]'>No tools available</p>
|
||||
) : (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{tools.map((tool) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
className='rounded-[6px] border bg-[var(--surface-3)] px-[10px] py-[8px]'
|
||||
>
|
||||
<p className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{tool.name}
|
||||
</p>
|
||||
{tool.description && (
|
||||
<p className='mt-[4px] text-[13px] text-[var(--text-tertiary)]'>
|
||||
{tool.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-auto flex items-center justify-end'>
|
||||
<Button
|
||||
onClick={handleBackToList}
|
||||
variant='primary'
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex h-full flex-col gap-[16px]'>
|
||||
@@ -524,6 +640,7 @@ export function MCP() {
|
||||
tools={tools}
|
||||
isDeleting={deletingServers.has(server.id)}
|
||||
onRemove={() => handleRemoveServer(server.id, server.name || 'this server')}
|
||||
onViewDetails={() => handleViewDetails(server.id)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -261,7 +261,7 @@ export function TemplateProfile() {
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
{/* Display Skeleton */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Skeleton className='h-[13px] w-[50px]' />
|
||||
<Skeleton className='h-5 w-[50px]' />
|
||||
<div className='flex items-center gap-[10px]'>
|
||||
<Skeleton className='h-9 w-9 flex-shrink-0 rounded-full' />
|
||||
<Skeleton className='h-9 flex-1' />
|
||||
@@ -270,13 +270,13 @@ export function TemplateProfile() {
|
||||
|
||||
{/* About Skeleton */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Skeleton className='h-[13px] w-[35px]' />
|
||||
<Skeleton className='h-5 w-[35px]' />
|
||||
<Skeleton className='min-h-[100px] w-full' />
|
||||
</div>
|
||||
|
||||
{/* Socials Skeleton */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Skeleton className='h-[13px] w-[45px]' />
|
||||
<Skeleton className='h-5 w-[45px]' />
|
||||
<Skeleton className='h-9 w-full' />
|
||||
<Skeleton className='h-9 w-full' />
|
||||
<Skeleton className='h-9 w-full' />
|
||||
|
||||
@@ -48,6 +48,7 @@ import { ssoKeys, useSSOProviders } from '@/hooks/queries/sso'
|
||||
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
|
||||
|
||||
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
|
||||
const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))
|
||||
|
||||
interface SettingsModalProps {
|
||||
open: boolean
|
||||
@@ -167,6 +168,18 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
return false
|
||||
}
|
||||
|
||||
// SSO has special logic that must be checked before requiresTeam
|
||||
if (item.id === 'sso') {
|
||||
if (isHosted) {
|
||||
return hasOrganization && hasEnterprisePlan && canManageSSO
|
||||
}
|
||||
// For self-hosted, only show SSO tab if explicitly enabled via environment variable
|
||||
if (!isSSOEnabled) return false
|
||||
// Show tab if user is the SSO provider owner, or if no providers exist yet (to allow initial setup)
|
||||
const hasProviders = (ssoProvidersData?.providers?.length ?? 0) > 0
|
||||
return !hasProviders || isSSOProviderOwner === true
|
||||
}
|
||||
|
||||
if (item.requiresTeam) {
|
||||
const isMember = userRole === 'member' || isAdmin
|
||||
const hasTeamPlan = subscriptionStatus.isTeam || subscriptionStatus.isEnterprise
|
||||
@@ -185,13 +198,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item.id === 'sso') {
|
||||
if (isHosted) {
|
||||
return hasOrganization && hasEnterprisePlan && canManageSSO
|
||||
}
|
||||
return isSSOProviderOwner === true
|
||||
}
|
||||
|
||||
if (item.requiresOwner && !isOwner) {
|
||||
return false
|
||||
}
|
||||
@@ -203,6 +209,8 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
hasEnterprisePlan,
|
||||
canManageSSO,
|
||||
isSSOProviderOwner,
|
||||
isSSOEnabled,
|
||||
ssoProvidersData?.providers?.length,
|
||||
isOwner,
|
||||
isAdmin,
|
||||
userRole,
|
||||
|
||||
@@ -13,6 +13,7 @@ export { Input } from './input/input'
|
||||
export { Label } from './label/label'
|
||||
export {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
type ModalContentProps,
|
||||
|
||||
@@ -45,7 +45,7 @@ const Content = React.forwardRef<
|
||||
collisionPadding={8}
|
||||
avoidCollisions={true}
|
||||
className={cn(
|
||||
'z-50 rounded-[3px] bg-black px-[7.5px] py-[6px] font-base text-white text-xs shadow-md dark:bg-white dark:text-black',
|
||||
'z-[10000300] rounded-[3px] bg-black px-[7.5px] py-[6px] font-base text-white text-xs shadow-md dark:bg-white dark:text-black',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -696,8 +696,8 @@ export function GrafanaIcon(props: SVGProps<SVGSVGElement>) {
|
||||
y2='5.356'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop stop-color='#FFF200' />
|
||||
<stop offset='1' stop-color='#F15A29' />
|
||||
<stop stopColor='#FFF200' />
|
||||
<stop offset='1' stopColor='#F15A29' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@@ -75,7 +75,7 @@ export function TagInput({
|
||||
placeholder={value.length === 0 ? placeholder : ''}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 text-[13px] focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-sm placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
value.length > 0 ? 'pl-[4px]' : 'pl-[4px]'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -50,10 +50,6 @@ export const usePanelEditorStore = create<PanelEditorState>()(
|
||||
},
|
||||
clearCurrentBlock: () => {
|
||||
set({ currentBlockId: null })
|
||||
|
||||
// When selection is cleared (e.g. clicking on the canvas), switch to the toolbar tab
|
||||
const panelState = usePanelStore.getState()
|
||||
panelState.setActiveTab('toolbar')
|
||||
},
|
||||
setConnectionsHeight: (height) => {
|
||||
const clampedHeight = Math.max(
|
||||
|
||||
Reference in New Issue
Block a user