mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-15 15:08:10 +01:00
♿ style(MCP): Enhance dialog accessibility and styling consistency (#11585)
* style: update input IDs in BasicInfoSection for consistency and improve accessibility * style: add border-destructive variable for improved design consistency * style: update error border color for title input in BasicInfoSection * style: update delete confirmation dialog title and description for MCP Server * style: add text-destructive variable for improved design consistency * style: update error message and border color for URL and trust fields for consistency * style: reorder imports and update error message styling for consistency across sections * style: enhance MCPServerDialog with copy link functionality and UI improvements * style: enhance MCPServerDialog with improved accessibility and loading indicators * style: bump @librechat/client to 0.4.51 and enhance OGDialogTemplate for improved selection handling * a11y: enhance accessibility and error handling in MCPServerDialog sections * style: enhance MCPServerDialog accessibility and improve resource name handling * style: improve accessibility in MCPServerDialog and AuthSection, update translation for delete confirmation * style: update aria-invalid attributes to use string values for improved accessibility in form sections * style: enhance accessibility in AuthSection by updating aria attributes and adding error messages * style: remove unnecessary aria-hidden attributes from Spinner components in MCPServerDialog * style: simplify legacy selection check in OGDialogTemplate
This commit is contained in:
parent
299efc2ccb
commit
d6b6f191f7
13 changed files with 295 additions and 141 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
import { FormProvider } from 'react-hook-form';
|
import { FormProvider } from 'react-hook-form';
|
||||||
|
import type { useMCPServerForm } from './hooks/useMCPServerForm';
|
||||||
import ConnectionSection from './sections/ConnectionSection';
|
import ConnectionSection from './sections/ConnectionSection';
|
||||||
import BasicInfoSection from './sections/BasicInfoSection';
|
import BasicInfoSection from './sections/BasicInfoSection';
|
||||||
import TransportSection from './sections/TransportSection';
|
import TransportSection from './sections/TransportSection';
|
||||||
import AuthSection from './sections/AuthSection';
|
|
||||||
import TrustSection from './sections/TrustSection';
|
import TrustSection from './sections/TrustSection';
|
||||||
import type { useMCPServerForm } from './hooks/useMCPServerForm';
|
import AuthSection from './sections/AuthSection';
|
||||||
|
|
||||||
interface MCPServerFormProps {
|
interface MCPServerFormProps {
|
||||||
formHook: ReturnType<typeof useMCPServerForm>;
|
formHook: ReturnType<typeof useMCPServerForm>;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Copy, CopyCheck } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
OGDialog,
|
Label,
|
||||||
OGDialogTemplate,
|
Input,
|
||||||
OGDialogContent,
|
|
||||||
OGDialogHeader,
|
|
||||||
OGDialogTitle,
|
|
||||||
Button,
|
Button,
|
||||||
TrashIcon,
|
|
||||||
Spinner,
|
Spinner,
|
||||||
|
TrashIcon,
|
||||||
|
useToastContext,
|
||||||
|
OGDialog,
|
||||||
|
OGDialogTitle,
|
||||||
|
OGDialogHeader,
|
||||||
|
OGDialogFooter,
|
||||||
|
OGDialogContent,
|
||||||
|
OGDialogTemplate,
|
||||||
} from '@librechat/client';
|
} from '@librechat/client';
|
||||||
import {
|
import {
|
||||||
SystemRoles,
|
SystemRoles,
|
||||||
|
|
@ -16,10 +21,10 @@ import {
|
||||||
PermissionBits,
|
PermissionBits,
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { GenericGrantAccessDialog } from '~/components/Sharing';
|
|
||||||
import { useAuthContext, useHasAccess, useResourcePermissions, MCPServerDefinition } from '~/hooks';
|
import { useAuthContext, useHasAccess, useResourcePermissions, MCPServerDefinition } from '~/hooks';
|
||||||
import { useLocalize } from '~/hooks';
|
import { GenericGrantAccessDialog } from '~/components/Sharing';
|
||||||
import { useMCPServerForm } from './hooks/useMCPServerForm';
|
import { useMCPServerForm } from './hooks/useMCPServerForm';
|
||||||
|
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||||
import MCPServerForm from './MCPServerForm';
|
import MCPServerForm from './MCPServerForm';
|
||||||
|
|
||||||
interface MCPServerDialogProps {
|
interface MCPServerDialogProps {
|
||||||
|
|
@ -39,8 +44,10 @@ export default function MCPServerDialog({
|
||||||
}: MCPServerDialogProps) {
|
}: MCPServerDialogProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
// State for dialogs
|
// State for dialogs
|
||||||
|
const [isCopying, setIsCopying] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [showRedirectUriDialog, setShowRedirectUriDialog] = useState(false);
|
const [showRedirectUriDialog, setShowRedirectUriDialog] = useState(false);
|
||||||
const [createdServerId, setCreatedServerId] = useState<string | null>(null);
|
const [createdServerId, setCreatedServerId] = useState<string | null>(null);
|
||||||
|
|
@ -99,20 +106,26 @@ export default function MCPServerDialog({
|
||||||
? `${window.location.origin}/api/mcp/${createdServerId}/oauth/callback`
|
? `${window.location.origin}/api/mcp/${createdServerId}/oauth/callback`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const copyLink = useCopyToClipboard({ text: redirectUri });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Delete confirmation dialog */}
|
{/* Delete confirmation dialog */}
|
||||||
<OGDialog open={showDeleteConfirm} onOpenChange={(isOpen) => setShowDeleteConfirm(isOpen)}>
|
<OGDialog open={showDeleteConfirm} onOpenChange={(isOpen) => setShowDeleteConfirm(isOpen)}>
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
title={localize('com_ui_delete')}
|
title={localize('com_ui_delete_mcp_server')}
|
||||||
className="max-w-[450px]"
|
className="w-11/12 max-w-md"
|
||||||
main={<p className="text-left text-sm">{localize('com_ui_mcp_server_delete_confirm')}</p>}
|
description={localize('com_ui_mcp_server_delete_confirm', { 0: server?.serverName })}
|
||||||
selection={{
|
selection={
|
||||||
selectHandler: handleDelete,
|
<Button
|
||||||
selectClasses:
|
onClick={handleDelete}
|
||||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
variant="destructive"
|
||||||
selectText: isDeleting ? <Spinner /> : localize('com_ui_delete'),
|
aria-live="polite"
|
||||||
}}
|
aria-label={isDeleting ? localize('com_ui_deleting') : localize('com_ui_delete')}
|
||||||
|
>
|
||||||
|
{isDeleting ? <Spinner /> : localize('com_ui_delete')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
|
|
||||||
|
|
@ -127,48 +140,53 @@ export default function MCPServerDialog({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OGDialogContent className="w-full max-w-lg border-none bg-surface-primary text-text-primary">
|
<OGDialogContent showCloseButton={false} className="w-11/12 max-w-lg">
|
||||||
<OGDialogHeader className="border-b border-border-light px-4 py-3">
|
<OGDialogHeader>
|
||||||
<OGDialogTitle>{localize('com_ui_mcp_server_created')}</OGDialogTitle>
|
<OGDialogTitle>{localize('com_ui_mcp_server_created')}</OGDialogTitle>
|
||||||
</OGDialogHeader>
|
</OGDialogHeader>
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-text-secondary">
|
<p className="text-sm">{localize('com_ui_redirect_uri_instructions')}</p>
|
||||||
{localize('com_ui_redirect_uri_instructions')}
|
|
||||||
</p>
|
<div className="space-y-2">
|
||||||
<div className="rounded-lg border border-border-medium bg-surface-secondary p-3">
|
<Label htmlFor="redirect-uri-input" className="text-sm font-medium">
|
||||||
<label className="mb-2 block text-xs font-medium text-text-secondary">
|
|
||||||
{localize('com_ui_redirect_uri')}
|
{localize('com_ui_redirect_uri')}
|
||||||
</label>
|
</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<Input
|
||||||
className="flex-1 rounded border border-border-medium bg-surface-primary px-3 py-2 text-sm"
|
id="redirect-uri-input"
|
||||||
value={redirectUri}
|
type="text"
|
||||||
readOnly
|
readOnly
|
||||||
|
value={redirectUri}
|
||||||
|
className="flex-1 text-text-secondary"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
size="icon"
|
||||||
navigator.clipboard.writeText(redirectUri);
|
|
||||||
}}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="whitespace-nowrap"
|
onClick={() => {
|
||||||
|
if (isCopying) return;
|
||||||
|
showToast({ message: localize('com_ui_copied_to_clipboard') });
|
||||||
|
copyLink(setIsCopying);
|
||||||
|
}}
|
||||||
|
disabled={isCopying}
|
||||||
|
className="p-0"
|
||||||
|
aria-label={localize('com_ui_copy_link')}
|
||||||
>
|
>
|
||||||
{localize('com_ui_copy_link')}
|
{isCopying ? <CopyCheck className="size-4" /> : <Copy className="size-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<OGDialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
variant="default"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowRedirectUriDialog(false);
|
setShowRedirectUriDialog(false);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setCreatedServerId(null);
|
setCreatedServerId(null);
|
||||||
}}
|
}}
|
||||||
variant="submit"
|
|
||||||
className="text-white"
|
|
||||||
>
|
>
|
||||||
{localize('com_ui_done')}
|
{localize('com_ui_done')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</OGDialogFooter>
|
||||||
</div>
|
</div>
|
||||||
</OGDialogContent>
|
</OGDialogContent>
|
||||||
</OGDialog>
|
</OGDialog>
|
||||||
|
|
@ -187,6 +205,7 @@ export default function MCPServerDialog({
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
showCloseButton={false}
|
||||||
className="w-11/12 md:max-w-3xl"
|
className="w-11/12 md:max-w-3xl"
|
||||||
main={<MCPServerForm formHook={formHook} />}
|
main={<MCPServerForm formHook={formHook} />}
|
||||||
footerClassName="sm:justify-between"
|
footerClassName="sm:justify-between"
|
||||||
|
|
@ -194,16 +213,15 @@ export default function MCPServerDialog({
|
||||||
isEditMode ? (
|
isEditMode ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
variant="destructive"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label={localize('com_ui_delete')}
|
aria-label={localize('com_ui_delete_mcp_server_name', {
|
||||||
|
0: server?.config?.title || server?.serverName || '',
|
||||||
|
})}
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
disabled={isSubmitting || isDeleting}
|
disabled={isSubmitting || isDeleting}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-center gap-2 text-red-500">
|
<TrashIcon aria-hidden="true" />
|
||||||
<TrashIcon />
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
{shouldShowShareButton && server && (
|
{shouldShowShareButton && server && (
|
||||||
<GenericGrantAccessDialog
|
<GenericGrantAccessDialog
|
||||||
|
|
@ -218,10 +236,15 @@ export default function MCPServerDialog({
|
||||||
buttons={
|
buttons={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="submit"
|
variant={isEditMode ? 'default' : 'submit'}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="text-white"
|
aria-live="polite"
|
||||||
|
aria-label={
|
||||||
|
isSubmitting
|
||||||
|
? localize(isEditMode ? 'com_ui_updating' : 'com_ui_creating')
|
||||||
|
: localize(isEditMode ? 'com_ui_update_mcp_server' : 'com_ui_create_mcp_server')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<Spinner className="size-4" />
|
<Spinner className="size-4" />
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Copy, CopyCheck } from 'lucide-react';
|
||||||
import { useFormContext, useWatch } from 'react-hook-form';
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
import { Label, Input, Checkbox, SecretInput, Radio, useToastContext } from '@librechat/client';
|
import { Label, Input, Checkbox, SecretInput, Radio, useToastContext } from '@librechat/client';
|
||||||
import { Copy, CopyCheck } from 'lucide-react';
|
|
||||||
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
|
||||||
import { cn } from '~/utils';
|
|
||||||
import { AuthTypeEnum, AuthorizationTypeEnum } from '../hooks/useMCPServerForm';
|
import { AuthTypeEnum, AuthorizationTypeEnum } from '../hooks/useMCPServerForm';
|
||||||
import type { MCPServerFormData } from '../hooks/useMCPServerForm';
|
import type { MCPServerFormData } from '../hooks/useMCPServerForm';
|
||||||
|
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface AuthSectionProps {
|
interface AuthSectionProps {
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
|
|
@ -62,15 +62,20 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Auth Type Radio */}
|
{/* Auth Type Radio */}
|
||||||
<div className="space-y-1.5">
|
<fieldset className="space-y-1.5">
|
||||||
<Label className="text-sm font-medium">{localize('com_ui_authentication')}</Label>
|
<legend>
|
||||||
|
<Label id="auth-type-label" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_authentication')}
|
||||||
|
</Label>
|
||||||
|
</legend>
|
||||||
<Radio
|
<Radio
|
||||||
options={authTypeOptions}
|
options={authTypeOptions}
|
||||||
value={authType || AuthTypeEnum.None}
|
value={authType || AuthTypeEnum.None}
|
||||||
onChange={(val) => setValue('auth.auth_type', val as AuthTypeEnum)}
|
onChange={(val) => setValue('auth.auth_type', val as AuthTypeEnum)}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
aria-labelledby="auth-type-label"
|
||||||
/>
|
/>
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
{/* API Key Fields */}
|
{/* API Key Fields */}
|
||||||
{authType === AuthTypeEnum.ServiceHttp && (
|
{authType === AuthTypeEnum.ServiceHttp && (
|
||||||
|
|
@ -83,9 +88,13 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setValue('auth.api_key_source', checked ? 'user' : 'admin')
|
setValue('auth.api_key_source', checked ? 'user' : 'admin')
|
||||||
}
|
}
|
||||||
aria-label={localize('com_ui_user_provides_key')}
|
aria-labelledby="user_provides_key_label"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="user_provides_key" className="cursor-pointer text-sm">
|
<label
|
||||||
|
id="user_provides_key_label"
|
||||||
|
htmlFor="user_provides_key"
|
||||||
|
className="cursor-pointer text-sm"
|
||||||
|
>
|
||||||
{localize('com_ui_user_provides_key')}
|
{localize('com_ui_user_provides_key')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -101,8 +110,12 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Header Format Radio */}
|
{/* Header Format Radio */}
|
||||||
<div className="space-y-1.5">
|
<fieldset className="space-y-1.5">
|
||||||
<Label className="text-sm font-medium">{localize('com_ui_header_format')}</Label>
|
<legend>
|
||||||
|
<Label id="header-format-label" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_header_format')}
|
||||||
|
</Label>
|
||||||
|
</legend>
|
||||||
<Radio
|
<Radio
|
||||||
options={headerFormatOptions}
|
options={headerFormatOptions}
|
||||||
value={authorizationType || AuthorizationTypeEnum.Bearer}
|
value={authorizationType || AuthorizationTypeEnum.Bearer}
|
||||||
|
|
@ -110,8 +123,9 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
||||||
setValue('auth.api_key_authorization_type', val as AuthorizationTypeEnum)
|
setValue('auth.api_key_authorization_type', val as AuthorizationTypeEnum)
|
||||||
}
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
aria-labelledby="header-format-label"
|
||||||
/>
|
/>
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
{/* Custom header name */}
|
{/* Custom header name */}
|
||||||
{authorizationType === AuthorizationTypeEnum.Custom && (
|
{authorizationType === AuthorizationTypeEnum.Custom && (
|
||||||
|
|
@ -137,27 +151,67 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="oauth_client_id" className="text-sm font-medium">
|
<Label htmlFor="oauth_client_id" className="text-sm font-medium">
|
||||||
{localize('com_ui_client_id')}{' '}
|
{localize('com_ui_client_id')}{' '}
|
||||||
{!isEditMode && <span className="text-text-secondary">*</span>}
|
{!isEditMode && (
|
||||||
|
<>
|
||||||
|
<span aria-hidden="true" className="text-text-secondary">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="oauth_client_id"
|
id="oauth_client_id"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
||||||
|
aria-invalid={errors.auth?.oauth_client_id ? 'true' : 'false'}
|
||||||
|
aria-describedby={
|
||||||
|
errors.auth?.oauth_client_id ? 'oauth-client-id-error' : undefined
|
||||||
|
}
|
||||||
{...register('auth.oauth_client_id', { required: !isEditMode })}
|
{...register('auth.oauth_client_id', { required: !isEditMode })}
|
||||||
className={cn(errors.auth?.oauth_client_id && 'border-red-500')}
|
className={cn(errors.auth?.oauth_client_id && 'border-border-destructive')}
|
||||||
/>
|
/>
|
||||||
|
{errors.auth?.oauth_client_id && (
|
||||||
|
<p
|
||||||
|
id="oauth-client-id-error"
|
||||||
|
role="alert"
|
||||||
|
className="text-xs text-text-destructive"
|
||||||
|
>
|
||||||
|
{localize('com_ui_field_required')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="oauth_client_secret" className="text-sm font-medium">
|
<Label htmlFor="oauth_client_secret" className="text-sm font-medium">
|
||||||
{localize('com_ui_client_secret')}{' '}
|
{localize('com_ui_client_secret')}{' '}
|
||||||
{!isEditMode && <span className="text-text-secondary">*</span>}
|
{!isEditMode && (
|
||||||
|
<>
|
||||||
|
<span aria-hidden="true" className="text-text-secondary">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<SecretInput
|
<SecretInput
|
||||||
id="oauth_client_secret"
|
id="oauth_client_secret"
|
||||||
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
||||||
|
aria-invalid={errors.auth?.oauth_client_secret ? 'true' : 'false'}
|
||||||
|
aria-describedby={
|
||||||
|
errors.auth?.oauth_client_secret ? 'oauth-client-secret-error' : undefined
|
||||||
|
}
|
||||||
{...register('auth.oauth_client_secret', { required: !isEditMode })}
|
{...register('auth.oauth_client_secret', { required: !isEditMode })}
|
||||||
className={cn(errors.auth?.oauth_client_secret && 'border-red-500')}
|
className={cn(errors.auth?.oauth_client_secret && 'border-border-destructive')}
|
||||||
/>
|
/>
|
||||||
|
{errors.auth?.oauth_client_secret && (
|
||||||
|
<p
|
||||||
|
id="oauth-client-secret-error"
|
||||||
|
role="alert"
|
||||||
|
className="text-xs text-text-destructive"
|
||||||
|
>
|
||||||
|
{localize('com_ui_field_required')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -196,9 +250,12 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
||||||
{/* Redirect URI */}
|
{/* Redirect URI */}
|
||||||
{isEditMode && redirectUri && (
|
{isEditMode && redirectUri && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-sm font-medium">{localize('com_ui_redirect_uri')}</Label>
|
<Label htmlFor="auth-redirect-uri" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_redirect_uri')}
|
||||||
|
</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
id="auth-redirect-uri"
|
||||||
type="text"
|
type="text"
|
||||||
readOnly
|
readOnly
|
||||||
value={redirectUri}
|
value={redirectUri}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { Input, Label, TextareaAutosize } from '@librechat/client';
|
import { Input, Label, Textarea } from '@librechat/client';
|
||||||
|
import type { MCPServerFormData } from '../hooks/useMCPServerForm';
|
||||||
|
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
|
||||||
import type { MCPServerFormData } from '../hooks/useMCPServerForm';
|
|
||||||
|
|
||||||
export default function BasicInfoSection() {
|
export default function BasicInfoSection() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -36,13 +36,19 @@ export default function BasicInfoSection() {
|
||||||
<MCPIcon icon={iconValue} onIconChange={handleIconChange} />
|
<MCPIcon icon={iconValue} onIconChange={handleIconChange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-1.5 sm:flex-1">
|
<div className="w-full space-y-1.5 sm:flex-1">
|
||||||
<Label htmlFor="title" className="text-sm font-medium">
|
<Label htmlFor="mcp-title" className="text-sm font-medium">
|
||||||
{localize('com_ui_name')} <span className="text-text-secondary">*</span>
|
{localize('com_ui_name')}{' '}
|
||||||
|
<span aria-hidden="true" className="text-text-secondary">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="title"
|
id="mcp-title"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={localize('com_agents_mcp_name_placeholder')}
|
placeholder={localize('com_agents_mcp_name_placeholder')}
|
||||||
|
aria-invalid={errors.title ? 'true' : 'false'}
|
||||||
|
aria-describedby={errors.title ? 'mcp-title-error' : undefined}
|
||||||
{...register('title', {
|
{...register('title', {
|
||||||
required: localize('com_ui_field_required'),
|
required: localize('com_ui_field_required'),
|
||||||
pattern: {
|
pattern: {
|
||||||
|
|
@ -50,26 +56,26 @@ export default function BasicInfoSection() {
|
||||||
message: localize('com_ui_mcp_title_invalid'),
|
message: localize('com_ui_mcp_title_invalid'),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
className={cn(errors.title && 'border-red-500 focus:border-red-500')}
|
className={cn(errors.title && 'border-border-destructive')}
|
||||||
/>
|
/>
|
||||||
{errors.title && <p className="text-xs text-red-500">{errors.title.message}</p>}
|
{errors.title && (
|
||||||
|
<p id="mcp-title-error" role="alert" className="text-xs text-text-destructive">
|
||||||
|
{errors.title.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description - always visible, full width */}
|
{/* Description */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="description" className="text-sm font-medium">
|
<Label htmlFor="mcp-description" className="text-sm font-medium">
|
||||||
{localize('com_ui_description')}{' '}
|
{localize('com_ui_description')}{' '}
|
||||||
<span className="text-xs text-text-secondary">{localize('com_ui_optional')}</span>
|
<span className="text-xs text-text-secondary">{localize('com_ui_optional')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<TextareaAutosize
|
<Textarea
|
||||||
id="description"
|
id="mcp-description"
|
||||||
aria-label={localize('com_ui_description')}
|
|
||||||
placeholder={localize('com_agents_mcp_description_placeholder')}
|
placeholder={localize('com_agents_mcp_description_placeholder')}
|
||||||
{...register('description')}
|
{...register('description')}
|
||||||
minRows={2}
|
|
||||||
maxRows={4}
|
|
||||||
className="w-full resize-none rounded-lg border border-input bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,19 @@ export default function ConnectionSection() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="url" className="text-sm font-medium">
|
<Label htmlFor="url" className="text-sm font-medium">
|
||||||
{localize('com_ui_mcp_url')} <span className="text-text-secondary">*</span>
|
{localize('com_ui_mcp_url')}{' '}
|
||||||
|
<span aria-hidden="true" className="text-text-secondary">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="url"
|
id="url"
|
||||||
type="url"
|
type="url"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={localize('com_ui_mcp_server_url_placeholder')}
|
placeholder={localize('com_ui_mcp_server_url_placeholder')}
|
||||||
|
aria-invalid={errors.url ? 'true' : 'false'}
|
||||||
|
aria-describedby={errors.url ? 'url-error' : undefined}
|
||||||
{...register('url', {
|
{...register('url', {
|
||||||
required: localize('com_ui_field_required'),
|
required: localize('com_ui_field_required'),
|
||||||
validate: (value) => {
|
validate: (value) => {
|
||||||
|
|
@ -29,9 +35,13 @@ export default function ConnectionSection() {
|
||||||
return isValidUrl(normalized) || localize('com_ui_mcp_invalid_url');
|
return isValidUrl(normalized) || localize('com_ui_mcp_invalid_url');
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
className={cn(errors.url && 'border-red-500 focus:border-red-500')}
|
className={cn(errors.url && 'border-border-destructive')}
|
||||||
/>
|
/>
|
||||||
{errors.url && <p className="text-xs text-red-500">{errors.url.message}</p>}
|
{errors.url && (
|
||||||
|
<p id="url-error" role="alert" className="text-xs text-text-destructive">
|
||||||
|
{errors.url.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,19 @@ export default function TransportSection() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-2">
|
||||||
<Label className="text-sm font-medium">{localize('com_ui_mcp_transport')}</Label>
|
<legend>
|
||||||
|
<Label id="transport-label" className="text-sm font-medium">
|
||||||
|
{localize('com_ui_mcp_transport')}
|
||||||
|
</Label>
|
||||||
|
</legend>
|
||||||
<Radio
|
<Radio
|
||||||
options={transportOptions}
|
options={transportOptions}
|
||||||
value={transportType}
|
value={transportType}
|
||||||
onChange={handleTransportChange}
|
onChange={handleTransportChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
aria-labelledby="transport-label"
|
||||||
/>
|
/>
|
||||||
</div>
|
</fieldset>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,17 @@ export default function TrustSection() {
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
aria-labelledby="trust-label"
|
aria-labelledby="trust-label"
|
||||||
aria-describedby="trust-description"
|
aria-describedby={
|
||||||
|
errors.trust ? 'trust-description trust-error' : 'trust-description'
|
||||||
|
}
|
||||||
|
aria-invalid={errors.trust ? 'true' : 'false'}
|
||||||
|
aria-required="true"
|
||||||
className="mt-0.5"
|
className="mt-0.5"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label htmlFor="trust" className="flex cursor-pointer flex-col gap-0.5 text-sm">
|
||||||
id="trust-label"
|
<span id="trust-label" className="font-medium text-text-primary">
|
||||||
htmlFor="trust"
|
|
||||||
className="flex cursor-pointer flex-col gap-0.5 text-sm"
|
|
||||||
>
|
|
||||||
<span className="font-medium text-text-primary">
|
|
||||||
{startupConfig?.interface?.mcpServers?.trustCheckbox?.label ? (
|
{startupConfig?.interface?.mcpServers?.trustCheckbox?.label ? (
|
||||||
<span
|
<span
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|
@ -49,7 +49,9 @@ export default function TrustSection() {
|
||||||
) : (
|
) : (
|
||||||
localize('com_ui_trust_app')
|
localize('com_ui_trust_app')
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<span className="text-text-secondary">*</span>
|
<span aria-hidden="true" className="text-text-secondary">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span id="trust-description" className="text-xs font-normal text-text-secondary">
|
<span id="trust-description" className="text-xs font-normal text-text-secondary">
|
||||||
{startupConfig?.interface?.mcpServers?.trustCheckbox?.subLabel ? (
|
{startupConfig?.interface?.mcpServers?.trustCheckbox?.subLabel ? (
|
||||||
|
|
@ -68,7 +70,9 @@ export default function TrustSection() {
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{errors.trust && (
|
{errors.trust && (
|
||||||
<p className="mt-2 text-xs text-red-500">{localize('com_ui_field_required')}</p>
|
<p id="trust-error" role="alert" className="mt-2 text-xs text-text-destructive">
|
||||||
|
{localize('com_ui_field_required')}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -857,8 +857,11 @@
|
||||||
"com_ui_copy_url_to_clipboard": "Copy URL to clipboard",
|
"com_ui_copy_url_to_clipboard": "Copy URL to clipboard",
|
||||||
"com_ui_create": "Create",
|
"com_ui_create": "Create",
|
||||||
"com_ui_create_api_key": "Create API Key",
|
"com_ui_create_api_key": "Create API Key",
|
||||||
|
"com_ui_created": "Created",
|
||||||
|
"com_ui_creating": "Creating...",
|
||||||
"com_ui_create_assistant": "Create Assistant",
|
"com_ui_create_assistant": "Create Assistant",
|
||||||
"com_ui_create_link": "Create link",
|
"com_ui_create_link": "Create link",
|
||||||
|
"com_ui_create_mcp_server": "Create MCP server",
|
||||||
"com_ui_create_memory": "Create Memory",
|
"com_ui_create_memory": "Create Memory",
|
||||||
"com_ui_create_new_agent": "Create New Agent",
|
"com_ui_create_new_agent": "Create New Agent",
|
||||||
"com_ui_create_prompt": "Create Prompt",
|
"com_ui_create_prompt": "Create Prompt",
|
||||||
|
|
@ -893,6 +896,7 @@
|
||||||
"com_ui_decline": "I do not accept",
|
"com_ui_decline": "I do not accept",
|
||||||
"com_ui_default_post_request": "Default (POST request)",
|
"com_ui_default_post_request": "Default (POST request)",
|
||||||
"com_ui_delete": "Delete",
|
"com_ui_delete": "Delete",
|
||||||
|
"com_ui_deleting": "Deleting...",
|
||||||
"com_ui_delete_action": "Delete Action",
|
"com_ui_delete_action": "Delete Action",
|
||||||
"com_ui_delete_action_confirm": "Are you sure you want to delete this action?",
|
"com_ui_delete_action_confirm": "Are you sure you want to delete this action?",
|
||||||
"com_ui_delete_agent": "Delete Agent",
|
"com_ui_delete_agent": "Delete Agent",
|
||||||
|
|
@ -915,6 +919,8 @@
|
||||||
"com_ui_delete_tool": "Delete Tool",
|
"com_ui_delete_tool": "Delete Tool",
|
||||||
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
|
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
|
||||||
"com_ui_delete_tool_save_reminder": "Tool removed. Save the agent to apply changes.",
|
"com_ui_delete_tool_save_reminder": "Tool removed. Save the agent to apply changes.",
|
||||||
|
"com_ui_delete_mcp_server": "Delete MCP Server?",
|
||||||
|
"com_ui_delete_mcp_server_name": "Delete MCP server {{0}}",
|
||||||
"com_ui_deleted": "Deleted",
|
"com_ui_deleted": "Deleted",
|
||||||
"com_ui_deleting_file": "Deleting file...",
|
"com_ui_deleting_file": "Deleting file...",
|
||||||
"com_ui_descending": "Desc",
|
"com_ui_descending": "Desc",
|
||||||
|
|
@ -1111,7 +1117,7 @@
|
||||||
"com_ui_mcp_server": "MCP Server",
|
"com_ui_mcp_server": "MCP Server",
|
||||||
"com_ui_mcp_server_connection_failed": "Connection attempt to the provided MCP server failed. Please make sure the URL, the server type, and any authentication configuration are correct, then try again. Also ensure the URL is reachable.",
|
"com_ui_mcp_server_connection_failed": "Connection attempt to the provided MCP server failed. Please make sure the URL, the server type, and any authentication configuration are correct, then try again. Also ensure the URL is reachable.",
|
||||||
"com_ui_mcp_server_created": "MCP server created successfully",
|
"com_ui_mcp_server_created": "MCP server created successfully",
|
||||||
"com_ui_mcp_server_delete_confirm": "Are you sure you want to delete this MCP server?",
|
"com_ui_mcp_server_delete_confirm": "Are you sure you want to delete the {{0}} MCP server?",
|
||||||
"com_ui_mcp_server_deleted": "MCP server deleted successfully",
|
"com_ui_mcp_server_deleted": "MCP server deleted successfully",
|
||||||
"com_ui_mcp_server_role_editor": "MCP Server Editor",
|
"com_ui_mcp_server_role_editor": "MCP Server Editor",
|
||||||
"com_ui_mcp_server_role_editor_desc": "Can view, use, and edit MCP servers",
|
"com_ui_mcp_server_role_editor_desc": "Can view, use, and edit MCP servers",
|
||||||
|
|
@ -1438,6 +1444,8 @@
|
||||||
"com_ui_unset": "Unset",
|
"com_ui_unset": "Unset",
|
||||||
"com_ui_untitled": "Untitled",
|
"com_ui_untitled": "Untitled",
|
||||||
"com_ui_update": "Update",
|
"com_ui_update": "Update",
|
||||||
|
"com_ui_updating": "Updating...",
|
||||||
|
"com_ui_update_mcp_server": "Update MCP server",
|
||||||
"com_ui_upload": "Upload",
|
"com_ui_upload": "Upload",
|
||||||
"com_ui_upload_agent_avatar": "Successfully updated agent avatar",
|
"com_ui_upload_agent_avatar": "Successfully updated agent avatar",
|
||||||
"com_ui_upload_agent_avatar_label": "Upload agent avatar image",
|
"com_ui_upload_agent_avatar_label": "Upload agent avatar image",
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ html {
|
||||||
--text-secondary-alt: var(--gray-500);
|
--text-secondary-alt: var(--gray-500);
|
||||||
--text-tertiary: var(--gray-500);
|
--text-tertiary: var(--gray-500);
|
||||||
--text-warning: var(--amber-500);
|
--text-warning: var(--amber-500);
|
||||||
|
--text-destructive: var(--red-600);
|
||||||
--ring-primary: var(--gray-500);
|
--ring-primary: var(--gray-500);
|
||||||
--header-primary: var(--white);
|
--header-primary: var(--white);
|
||||||
--header-hover: var(--gray-50);
|
--header-hover: var(--gray-50);
|
||||||
|
|
@ -96,6 +97,7 @@ html {
|
||||||
--border-medium: var(--gray-300);
|
--border-medium: var(--gray-300);
|
||||||
--border-heavy: var(--gray-400);
|
--border-heavy: var(--gray-400);
|
||||||
--border-xheavy: var(--gray-500);
|
--border-xheavy: var(--gray-500);
|
||||||
|
--border-destructive: var(--red-600);
|
||||||
/* These are test styles */
|
/* These are test styles */
|
||||||
|
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
|
|
@ -131,6 +133,7 @@ html {
|
||||||
--text-secondary-alt: var(--gray-400);
|
--text-secondary-alt: var(--gray-400);
|
||||||
--text-tertiary: var(--gray-500);
|
--text-tertiary: var(--gray-500);
|
||||||
--text-warning: var(--amber-500);
|
--text-warning: var(--amber-500);
|
||||||
|
--text-destructive: var(--red-600);
|
||||||
--header-primary: var(--gray-700);
|
--header-primary: var(--gray-700);
|
||||||
--header-hover: var(--gray-600);
|
--header-hover: var(--gray-600);
|
||||||
--header-button-hover: var(--gray-700);
|
--header-button-hover: var(--gray-700);
|
||||||
|
|
@ -156,6 +159,7 @@ html {
|
||||||
--border-medium: var(--gray-600);
|
--border-medium: var(--gray-600);
|
||||||
--border-heavy: var(--gray-500);
|
--border-heavy: var(--gray-500);
|
||||||
--border-xheavy: var(--gray-400);
|
--border-xheavy: var(--gray-400);
|
||||||
|
--border-destructive: var(--red-500);
|
||||||
/* These are test styles */
|
/* These are test styles */
|
||||||
|
|
||||||
--background: 0 0% 7%;
|
--background: 0 0% 7%;
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ export const RESOURCE_CONFIGS: Record<ResourceType, ResourceConfig> = {
|
||||||
defaultEditorRoleId: AccessRoleIds.AGENT_EDITOR,
|
defaultEditorRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||||
defaultOwnerRoleId: AccessRoleIds.AGENT_OWNER,
|
defaultOwnerRoleId: AccessRoleIds.AGENT_OWNER,
|
||||||
getResourceUrl: (agentId: string) => `${window.location.origin}/c/new?agent_id=${agentId}`,
|
getResourceUrl: (agentId: string) => `${window.location.origin}/c/new?agent_id=${agentId}`,
|
||||||
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
|
getResourceName: (name?: string) => (name && name !== '' ? name : 'agent'),
|
||||||
getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
|
getShareMessage: (name?: string) => (name && name !== '' ? name : 'agent'),
|
||||||
getManageMessage: (name?: string) =>
|
getManageMessage: (name?: string) =>
|
||||||
`Manage permissions for ${name && name !== '' ? `"${name}"` : 'agent'}`,
|
`Manage permissions for ${name && name !== '' ? name : 'agent'}`,
|
||||||
getCopyUrlMessage: () => 'Agent URL copied',
|
getCopyUrlMessage: () => 'Agent URL copied',
|
||||||
},
|
},
|
||||||
[ResourceType.PROMPTGROUP]: {
|
[ResourceType.PROMPTGROUP]: {
|
||||||
|
|
@ -30,10 +30,10 @@ export const RESOURCE_CONFIGS: Record<ResourceType, ResourceConfig> = {
|
||||||
defaultViewerRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
defaultViewerRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
|
||||||
defaultEditorRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
|
defaultEditorRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
|
||||||
defaultOwnerRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
|
defaultOwnerRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
|
||||||
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
|
getResourceName: (name?: string) => (name && name !== '' ? name : 'prompt'),
|
||||||
getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
|
getShareMessage: (name?: string) => (name && name !== '' ? name : 'prompt'),
|
||||||
getManageMessage: (name?: string) =>
|
getManageMessage: (name?: string) =>
|
||||||
`Manage permissions for ${name && name !== '' ? `"${name}"` : 'prompt'}`,
|
`Manage permissions for ${name && name !== '' ? name : 'prompt'}`,
|
||||||
getCopyUrlMessage: () => 'Prompt URL copied',
|
getCopyUrlMessage: () => 'Prompt URL copied',
|
||||||
},
|
},
|
||||||
[ResourceType.MCPSERVER]: {
|
[ResourceType.MCPSERVER]: {
|
||||||
|
|
@ -41,10 +41,10 @@ export const RESOURCE_CONFIGS: Record<ResourceType, ResourceConfig> = {
|
||||||
defaultViewerRoleId: AccessRoleIds.MCPSERVER_VIEWER,
|
defaultViewerRoleId: AccessRoleIds.MCPSERVER_VIEWER,
|
||||||
defaultEditorRoleId: AccessRoleIds.MCPSERVER_EDITOR,
|
defaultEditorRoleId: AccessRoleIds.MCPSERVER_EDITOR,
|
||||||
defaultOwnerRoleId: AccessRoleIds.MCPSERVER_OWNER,
|
defaultOwnerRoleId: AccessRoleIds.MCPSERVER_OWNER,
|
||||||
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'MCP server'),
|
getResourceName: (name?: string) => (name && name !== '' ? name : 'MCP server'),
|
||||||
getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'MCP server'),
|
getShareMessage: (name?: string) => (name && name !== '' ? name : 'MCP server'),
|
||||||
getManageMessage: (name?: string) =>
|
getManageMessage: (name?: string) =>
|
||||||
`Manage permissions for ${name && name !== '' ? `"${name}"` : 'MCP server'}`,
|
`Manage permissions for ${name && name !== '' ? name : 'MCP server'}`,
|
||||||
getCopyUrlMessage: () => 'MCP Server URL copied',
|
getCopyUrlMessage: () => 'MCP Server URL copied',
|
||||||
},
|
},
|
||||||
[ResourceType.REMOTE_AGENT]: {
|
[ResourceType.REMOTE_AGENT]: {
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ module.exports = {
|
||||||
'text-secondary-alt': 'var(--text-secondary-alt)',
|
'text-secondary-alt': 'var(--text-secondary-alt)',
|
||||||
'text-tertiary': 'var(--text-tertiary)',
|
'text-tertiary': 'var(--text-tertiary)',
|
||||||
'text-warning': 'var(--text-warning)',
|
'text-warning': 'var(--text-warning)',
|
||||||
|
'text-destructive': 'var(--text-destructive)',
|
||||||
'ring-primary': 'var(--ring-primary)',
|
'ring-primary': 'var(--ring-primary)',
|
||||||
'header-primary': 'var(--header-primary)',
|
'header-primary': 'var(--header-primary)',
|
||||||
'header-hover': 'var(--header-hover)',
|
'header-hover': 'var(--header-hover)',
|
||||||
|
|
@ -118,6 +119,7 @@ module.exports = {
|
||||||
'border-medium-alt': 'var(--border-medium-alt)',
|
'border-medium-alt': 'var(--border-medium-alt)',
|
||||||
'border-heavy': 'var(--border-heavy)',
|
'border-heavy': 'var(--border-heavy)',
|
||||||
'border-xheavy': 'var(--border-xheavy)',
|
'border-xheavy': 'var(--border-xheavy)',
|
||||||
|
'border-destructive': 'var(--border-destructive)',
|
||||||
/* These are test styles */
|
/* These are test styles */
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
input: 'hsl(var(--input))',
|
input: 'hsl(var(--input))',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { forwardRef, ReactNode, Ref } from 'react';
|
import { forwardRef, isValidElement, ReactNode, Ref } from 'react';
|
||||||
import {
|
import {
|
||||||
OGDialogTitle,
|
OGDialogTitle,
|
||||||
OGDialogClose,
|
OGDialogClose,
|
||||||
|
|
@ -19,13 +19,39 @@ type SelectionProps = {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if selection is a legacy SelectionProps object
|
||||||
|
*/
|
||||||
|
function isSelectionProps(selection: unknown): selection is SelectionProps {
|
||||||
|
return (
|
||||||
|
typeof selection === 'object' &&
|
||||||
|
selection !== null &&
|
||||||
|
!isValidElement(selection) &&
|
||||||
|
('selectHandler' in selection ||
|
||||||
|
'selectClasses' in selection ||
|
||||||
|
'selectText' in selection ||
|
||||||
|
'isLoading' in selection)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type DialogTemplateProps = {
|
type DialogTemplateProps = {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
main?: ReactNode;
|
main?: ReactNode;
|
||||||
buttons?: ReactNode;
|
buttons?: ReactNode;
|
||||||
leftButtons?: ReactNode;
|
leftButtons?: ReactNode;
|
||||||
selection?: SelectionProps;
|
/**
|
||||||
|
* Selection button configuration. Can be either:
|
||||||
|
* - An object with selectHandler, selectClasses, selectText, isLoading (legacy)
|
||||||
|
* - A ReactNode for custom selection component
|
||||||
|
* @example
|
||||||
|
* // Legacy usage
|
||||||
|
* selection={{ selectHandler: () => {}, selectText: 'Confirm' }}
|
||||||
|
* @example
|
||||||
|
* // Custom component
|
||||||
|
* selection={<Button onClick={handleConfirm}>Confirm</Button>}
|
||||||
|
*/
|
||||||
|
selection?: SelectionProps | ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
overlayClassName?: string;
|
overlayClassName?: string;
|
||||||
headerClassName?: string;
|
headerClassName?: string;
|
||||||
|
|
@ -49,14 +75,39 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
||||||
mainClassName,
|
mainClassName,
|
||||||
headerClassName,
|
headerClassName,
|
||||||
footerClassName,
|
footerClassName,
|
||||||
showCloseButton,
|
showCloseButton = false,
|
||||||
overlayClassName,
|
overlayClassName,
|
||||||
showCancelButton = true,
|
showCancelButton = true,
|
||||||
} = props;
|
} = props;
|
||||||
const { selectHandler, selectClasses, selectText, isLoading } = selection || {};
|
const isLegacySelection = isSelectionProps(selection);
|
||||||
|
const { selectHandler, selectClasses, selectText, isLoading } = isLegacySelection
|
||||||
|
? selection
|
||||||
|
: {};
|
||||||
|
|
||||||
const defaultSelect =
|
const defaultSelect =
|
||||||
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
|
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
|
||||||
|
|
||||||
|
let selectionContent = null;
|
||||||
|
if (isLegacySelection) {
|
||||||
|
selectionContent = (
|
||||||
|
<OGDialogClose
|
||||||
|
onClick={selectHandler}
|
||||||
|
disabled={isLoading}
|
||||||
|
className={`${
|
||||||
|
selectClasses ?? defaultSelect
|
||||||
|
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80 max-sm:order-first max-sm:w-full sm:order-none`}
|
||||||
|
>
|
||||||
|
{isLoading === true ? (
|
||||||
|
<Spinner className="size-4 text-text-primary" />
|
||||||
|
) : (
|
||||||
|
(selectText as React.JSX.Element)
|
||||||
|
)}
|
||||||
|
</OGDialogClose>
|
||||||
|
);
|
||||||
|
} else if (selection) {
|
||||||
|
selectionContent = selection;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OGDialogContent
|
<OGDialogContent
|
||||||
overlayClassName={overlayClassName}
|
overlayClassName={overlayClassName}
|
||||||
|
|
@ -75,38 +126,18 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
|
||||||
</OGDialogHeader>
|
</OGDialogHeader>
|
||||||
<div className={cn('px-0 py-2', mainClassName)}>{main != null ? main : null}</div>
|
<div className={cn('px-0 py-2', mainClassName)}>{main != null ? main : null}</div>
|
||||||
<OGDialogFooter className={footerClassName}>
|
<OGDialogFooter className={footerClassName}>
|
||||||
<div>
|
{leftButtons != null ? (
|
||||||
{leftButtons != null ? (
|
<div className="mr-auto flex flex-row gap-2">{leftButtons}</div>
|
||||||
<div className="mt-3 flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:mt-0 sm:flex-row">
|
) : null}
|
||||||
{leftButtons}
|
{showCancelButton && (
|
||||||
</div>
|
<OGDialogClose asChild>
|
||||||
) : null}
|
<Button variant="outline" aria-label={localize('com_ui_cancel')}>
|
||||||
</div>
|
{localize('com_ui_cancel')}
|
||||||
<div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row">
|
</Button>
|
||||||
{showCancelButton && (
|
</OGDialogClose>
|
||||||
<OGDialogClose asChild>
|
)}
|
||||||
<Button variant="outline" aria-label={localize('com_ui_cancel')}>
|
{buttons != null ? buttons : null}
|
||||||
{localize('com_ui_cancel')}
|
{selectionContent}
|
||||||
</Button>
|
|
||||||
</OGDialogClose>
|
|
||||||
)}
|
|
||||||
{buttons != null ? buttons : null}
|
|
||||||
{selection ? (
|
|
||||||
<OGDialogClose
|
|
||||||
onClick={selectHandler}
|
|
||||||
disabled={isLoading}
|
|
||||||
className={`${
|
|
||||||
selectClasses ?? defaultSelect
|
|
||||||
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80 max-sm:order-first max-sm:w-full sm:order-none`}
|
|
||||||
>
|
|
||||||
{isLoading === true ? (
|
|
||||||
<Spinner className="size-4 text-white" />
|
|
||||||
) : (
|
|
||||||
(selectText as React.JSX.Element)
|
|
||||||
)}
|
|
||||||
</OGDialogClose>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</OGDialogFooter>
|
</OGDialogFooter>
|
||||||
</OGDialogContent>
|
</OGDialogContent>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ interface RadioProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
'aria-labelledby'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Radio = memo(function Radio({
|
const Radio = memo(function Radio({
|
||||||
|
|
@ -23,6 +24,7 @@ const Radio = memo(function Radio({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className = '',
|
className = '',
|
||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
|
'aria-labelledby': ariaLabelledBy,
|
||||||
}: RadioProps) {
|
}: RadioProps) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||||
|
|
@ -79,6 +81,7 @@ const Radio = memo(function Radio({
|
||||||
<div
|
<div
|
||||||
className="relative inline-flex items-center rounded-lg bg-muted p-1 opacity-50"
|
className="relative inline-flex items-center rounded-lg bg-muted p-1 opacity-50"
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
>
|
>
|
||||||
<span className="px-4 py-2 text-xs text-muted-foreground">
|
<span className="px-4 py-2 text-xs text-muted-foreground">
|
||||||
{localize('com_ui_no_options')}
|
{localize('com_ui_no_options')}
|
||||||
|
|
@ -93,6 +96,7 @@ const Radio = memo(function Radio({
|
||||||
<div
|
<div
|
||||||
className={`relative ${fullWidth ? 'flex' : 'inline-flex'} items-center rounded-lg bg-muted p-1 ${className}`}
|
className={`relative ${fullWidth ? 'flex' : 'inline-flex'} items-center rounded-lg bg-muted p-1 ${className}`}
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
>
|
>
|
||||||
{selectedIndex >= 0 && isMounted && (
|
{selectedIndex >= 0 && isMounted && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue