feat(sandbox-provider): update UI to match Figma design

- Update settings icon to RiEqualizer2Line
- Add 4px rounded container for provider icons in config modal
- Update section titles to uppercase style
- Change switch modal confirm button to warning variant
- Add i18n keys for setAsActive, readDocLink, securityTip
This commit is contained in:
yyh 2026-01-13 11:04:11 +08:00
parent 9eafe982ee
commit a81d0327d2
No known key found for this signature in database
6 changed files with 151 additions and 84 deletions

View File

@ -2,7 +2,7 @@
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
import type { SandboxProvider } from '@/service/use-sandbox-provider'
import { RiExternalLinkLine } from '@remixicon/react'
import { RiExternalLinkLine, RiLock2Fill } from '@remixicon/react'
import { memo, useCallback, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
@ -22,6 +22,27 @@ type ConfigModalProps = {
onClose: () => void
}
const PROVIDER_ICONS: Record<string, string> = {
e2b: '/sandbox-providers/e2b.svg',
daytona: '/sandbox-providers/daytona.svg',
docker: '/sandbox-providers/docker.svg',
local: '/sandbox-providers/local.svg',
}
const ProviderIcon = ({ providerType }: { providerType: string }) => {
const iconSrc = PROVIDER_ICONS[providerType] || PROVIDER_ICONS.e2b
return (
<div className="flex h-4 w-4 shrink-0 items-center justify-center text-clip rounded border-[0.5px] border-divider-subtle">
<img
src={iconSrc}
alt={`${providerType} icon`}
className="h-4 w-4"
/>
</div>
)
}
const ConfigModal = ({
provider,
onClose,
@ -91,54 +112,78 @@ const ConfigModal = ({
<Modal
isShow
onClose={onClose}
title={t('sandboxProvider.configModal.title', { ns: 'common', provider: provider.label })}
title={t('sandboxProvider.configModal.title', { ns: 'common' })}
closable
className="w-[480px]"
>
<div className="mt-4">
<BaseForm
formSchemas={formSchemas}
ref={formRef}
labelClassName="system-sm-semibold mb-1 flex items-center gap-1 text-text-secondary"
formClassName="space-y-4"
/>
{/* Provider subtitle */}
<div className="-mt-2 mb-4 flex items-center gap-2">
<ProviderIcon providerType={provider.provider_type} />
<span className="system-md-regular text-text-secondary">{provider.label}</span>
</div>
{/* Footer Actions */}
<div className="mt-6 flex items-center justify-between">
<div>
{docLink && (
<a
href={docLink}
target="_blank"
rel="noopener noreferrer"
className="system-sm-medium inline-flex items-center gap-1 text-text-accent hover:underline"
>
{t('sandboxProvider.configModal.readDoc', { ns: 'common' })}
<RiExternalLinkLine className="h-3.5 w-3.5" />
</a>
)}
</div>
<div className="flex items-center gap-2">
{isConfigured && (
<Button
variant="warning"
size="medium"
onClick={handleRevoke}
disabled={isDeleting || isSaving}
>
{t('sandboxProvider.configModal.revoke', { ns: 'common' })}
</Button>
)}
<Button
variant="primary"
size="medium"
onClick={handleSave}
disabled={isSaving || isDeleting}
<BaseForm
formSchemas={formSchemas}
ref={formRef}
labelClassName="system-sm-medium mb-1 flex items-center gap-1 text-text-secondary"
formClassName="space-y-4"
/>
{/* Footer Actions */}
<div className="mt-6 flex items-center justify-between">
<div>
{docLink && (
<a
href={docLink}
target="_blank"
rel="noopener noreferrer"
className="system-xs-regular inline-flex items-center gap-1 text-text-accent hover:underline"
>
{t('sandboxProvider.configModal.confirm', { ns: 'common' })}
</Button>
</div>
{t('sandboxProvider.configModal.readDocLink', { ns: 'common', provider: provider.label })}
<RiExternalLinkLine className="h-3 w-3" />
</a>
)}
</div>
<div className="flex items-center gap-2">
{isConfigured && (
<Button
variant="warning"
size="medium"
onClick={handleRevoke}
disabled={isDeleting || isSaving}
>
{t('sandboxProvider.configModal.revoke', { ns: 'common' })}
</Button>
)}
<Button
variant="secondary"
size="medium"
onClick={onClose}
disabled={isSaving || isDeleting}
>
{t('sandboxProvider.configModal.cancel', { ns: 'common' })}
</Button>
<Button
variant="primary"
size="medium"
onClick={handleSave}
disabled={isSaving || isDeleting}
>
{t('sandboxProvider.configModal.save', { ns: 'common' })}
</Button>
</div>
</div>
{/* Security tip */}
<div className="-mx-6 -mb-6 mt-4 flex items-start justify-center gap-1 rounded-b-2xl border-t border-divider-subtle bg-background-soft px-2 py-3">
<RiLock2Fill className="h-3 w-3 shrink-0 text-text-primary" />
<p className="system-xs-regular text-text-tertiary">
{t('sandboxProvider.configModal.securityTip', { ns: 'common' })}
{' '}
<span className="text-text-accent">PKCS1_OAEP</span>
{' '}
{t('sandboxProvider.configModal.securityTipTechnology', { ns: 'common' })}
</p>
</div>
</Modal>
)

View File

@ -41,7 +41,7 @@ const SandboxProviderPage = () => {
{/* Current Provider Section */}
{currentProvider && (
<div>
<div className="system-sm-semibold mb-2 text-text-secondary">
<div className="system-sm-semibold-uppercase mb-2 text-text-secondary">
{t('sandboxProvider.currentProvider', { ns: 'common' })}
</div>
<ProviderCard
@ -56,7 +56,7 @@ const SandboxProviderPage = () => {
{/* Other Providers Section */}
{otherProviders.length > 0 && (
<div>
<div className="system-sm-semibold mb-2 text-text-secondary">
<div className="system-sm-semibold-uppercase mb-2 text-text-secondary">
{t('sandboxProvider.otherProvider', { ns: 'common' })}
</div>
<div className="space-y-2">

View File

@ -1,6 +1,7 @@
'use client'
import type { SandboxProvider } from '@/service/use-sandbox-provider'
import { RiEqualizer2Line } from '@remixicon/react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
@ -17,6 +18,7 @@ type ProviderCardProps = {
const PROVIDER_ICONS: Record<string, string> = {
e2b: '/sandbox-providers/e2b.svg',
daytona: '/sandbox-providers/daytona.svg',
docker: '/sandbox-providers/docker.svg',
local: '/sandbox-providers/local.svg',
}
@ -28,7 +30,7 @@ const ProviderIcon = ({ providerType }: { providerType: string }) => {
<img
src={iconSrc}
alt={`${providerType} icon`}
className="h-5 w-5"
className="h-6 w-6"
/>
)
}
@ -47,50 +49,62 @@ const ProviderCard = ({
return (
<div className={cn(
'flex items-center justify-between rounded-xl p-4',
'flex items-center gap-3 rounded-[15px] py-3 pl-3 pr-4',
'border-[0.5px] border-components-panel-border shadow-xs',
isCurrent ? 'bg-background-section' : 'bg-background-section-burn',
)}
>
<div className="flex items-center">
{/* Icon */}
<div className="mr-3 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-divider-subtle bg-background-default-subtle">
<ProviderIcon providerType={provider.provider_type} />
</div>
{/* Icon - 40x40 with 10px rounded */}
<div className="flex h-10 w-10 shrink-0 items-center justify-center text-clip rounded-[10px] border-[0.5px] border-divider-subtle bg-background-default-subtle">
<ProviderIcon providerType={provider.provider_type} />
</div>
{/* Content */}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="system-md-semibold text-text-primary">
{provider.label}
{/* Content */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1">
<span className="system-md-semibold text-text-primary">
{provider.label}
</span>
{provider.is_system_configured && (
<span className="system-2xs-medium rounded-[5px] border border-divider-deep px-[5px] py-[3px] text-text-tertiary">
{t('sandboxProvider.managedBySaas', { ns: 'common' })}
</span>
{provider.is_system_configured && (
<span className="system-2xs-medium-uppercase rounded border border-divider-regular px-1.5 py-0.5 text-text-tertiary">
{t('sandboxProvider.managedBySaas', { ns: 'common' })}
</span>
)}
</div>
<div className="system-xs-regular text-text-tertiary">
{provider.description}
</div>
)}
</div>
<div className="system-xs-regular text-text-tertiary">
{provider.description}
</div>
</div>
{/* Right side: Connected Badge + Actions */}
{/* Right side: Connected Badge + Divider + Settings Icon + Enable Button */}
<div className="flex shrink-0 items-center gap-2">
{/* Connected Status */}
{isConfigured && (
<span className="system-xs-medium flex items-center gap-1 rounded-md bg-util-colors-green-green-50 px-1.5 py-0.5 text-util-colors-green-green-600">
<span className="flex items-center gap-1">
<Indicator color="green" />
{t('sandboxProvider.connected', { ns: 'common' })}
<span className="system-xs-semibold-uppercase text-util-colors-green-green-600">
{t('sandboxProvider.connected', { ns: 'common' })}
</span>
</span>
)}
<Button
variant="secondary"
size="small"
{/* Divider */}
{isConfigured && (
<div className="pl-1">
<div className="h-3 w-px bg-divider-regular" />
</div>
)}
{/* Settings Icon Button */}
<button
onClick={onConfig}
disabled={disabled}
className="flex h-8 w-8 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:opacity-50"
>
{t('sandboxProvider.config', { ns: 'common' })}
</Button>
<RiEqualizer2Line className="h-4 w-4" />
</button>
{/* Set as Active Button */}
{showEnableButton && (
<Button
variant="secondary"
@ -98,7 +112,7 @@ const ProviderCard = ({
onClick={onEnable}
disabled={disabled}
>
{t('sandboxProvider.enable', { ns: 'common' })}
{t('sandboxProvider.setAsActive', { ns: 'common' })}
</Button>
)}
</div>

View File

@ -79,7 +79,7 @@ const SwitchModal = ({
{t('sandboxProvider.switchModal.cancel', { ns: 'common' })}
</Button>
<Button
variant="primary"
variant="warning"
size="medium"
onClick={handleConfirm}
disabled={isPending}

View File

@ -571,11 +571,14 @@
"sandboxProvider.configModal.e2bTemplate": "E2B Template",
"sandboxProvider.configModal.e2bTemplatePlaceholder": "code-interpreter-v1",
"sandboxProvider.configModal.readDoc": "Read Documentation",
"sandboxProvider.configModal.readDocLink": "Read {{provider}} Docs",
"sandboxProvider.configModal.revoke": "Revoke",
"sandboxProvider.configModal.save": "Save",
"sandboxProvider.configModal.title": "Configure - {{provider}}",
"sandboxProvider.connected": "Connected",
"sandboxProvider.currentProvider": "Current Provider",
"sandboxProvider.configModal.securityTip": "Your API Token will be encrypted and stored using",
"sandboxProvider.configModal.securityTipTechnology": "technology.",
"sandboxProvider.configModal.title": "Configure Sandbox Provider",
"sandboxProvider.connected": "CONNECTED",
"sandboxProvider.currentProvider": "CURRENT ACTIVE",
"sandboxProvider.daytona.description": "Secure sandboxed cloud environments for AI agents. learn more",
"sandboxProvider.daytona.label": "Daytona",
"sandboxProvider.docker.description": "Secure sandboxed cloud environments for AI agents. learn more",
@ -588,9 +591,10 @@
"sandboxProvider.managedBySaas": "Managed by SaaS",
"sandboxProvider.noPermission": "Contact the workspace administrator to make changes.",
"sandboxProvider.notConfigured": "Not Configured",
"sandboxProvider.otherProvider": "Other Provider",
"sandboxProvider.otherProvider": "OTHER PROVIDERS",
"sandboxProvider.setAsActive": "Set as Active",
"sandboxProvider.switchModal.cancel": "Cancel",
"sandboxProvider.switchModal.confirm": "Switch Provider",
"sandboxProvider.switchModal.confirm": "Switch",
"sandboxProvider.switchModal.confirmText": "You are about to switch the active sandbox provider to {{provider}}.",
"sandboxProvider.switchModal.title": "Switch Active Provider?",
"sandboxProvider.switchModal.warning": "Impact on running agents",

View File

@ -569,11 +569,14 @@
"sandboxProvider.configModal.e2bTemplate": "E2B 模板",
"sandboxProvider.configModal.e2bTemplatePlaceholder": "code-interpreter-v1",
"sandboxProvider.configModal.readDoc": "查看文档",
"sandboxProvider.configModal.readDocLink": "查看 {{provider}} 文档",
"sandboxProvider.configModal.revoke": "撤销",
"sandboxProvider.configModal.save": "保存",
"sandboxProvider.configModal.title": "配置 - {{provider}}",
"sandboxProvider.configModal.securityTip": "您的 API Token 将使用",
"sandboxProvider.configModal.securityTipTechnology": "技术加密存储。",
"sandboxProvider.configModal.title": "配置 Sandbox Provider",
"sandboxProvider.connected": "已连接",
"sandboxProvider.currentProvider": "当前供应商",
"sandboxProvider.currentProvider": "当前激活",
"sandboxProvider.daytona.description": "为 AI 代理提供安全的沙箱云环境。了解更多",
"sandboxProvider.daytona.label": "Daytona",
"sandboxProvider.docker.description": "为 AI 代理提供安全的沙箱云环境。了解更多",
@ -587,8 +590,9 @@
"sandboxProvider.noPermission": "请联系工作空间管理员进行更改。",
"sandboxProvider.notConfigured": "未配置",
"sandboxProvider.otherProvider": "其他供应商",
"sandboxProvider.setAsActive": "设为激活",
"sandboxProvider.switchModal.cancel": "取消",
"sandboxProvider.switchModal.confirm": "切换供应商",
"sandboxProvider.switchModal.confirm": "切换",
"sandboxProvider.switchModal.confirmText": "您即将将活动沙箱供应商切换为 {{provider}}。",
"sandboxProvider.switchModal.title": "切换活动供应商?",
"sandboxProvider.switchModal.warning": "对运行中的代理的影响",