refactor(frontend): P1-P2 续 — API 子模块化、大组件拆分、Query 铺开
- api/server/ 与 api/cloud/ 子目录拆分,保持 @/api 导入不变 - hooks/api 新增 useCredentials、usePromDatasources,增强 useServers - 拆分 CredentialManager、DeploymentWorkload、PrometheusDatasourceManager - ServerManager、CredentialManager、PromDs 接入 TanStack Query
This commit is contained in:
1 parent
f45e203e21
commit
480e677042
32 files changed
+2280
-1940
No files matched your search
@@ -0,0 +1,395 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Key, Eye, EyeOff } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { credentialApi } from '@/api';
|
||||
import type { Credential, SecretType } from './types';
|
||||
import { TYPE_CONFIG } from './constants';
|
||||
|
||||
export interface CredFormDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
editData: Credential | null;
|
||||
}
|
||||
|
||||
// 凭据新增/编辑弹窗 // Logan.Li
|
||||
export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDialogProps) {
|
||||
const isEdit = !!editData;
|
||||
const [name, setName] = useState('');
|
||||
const [varName, setVarName] = useState('');
|
||||
const [secretType, setSecretType] = useState<SecretType>('ssh');
|
||||
const [description, setDescription] = useState('');
|
||||
const [tags, setTags] = useState('');
|
||||
const [isActive, setIsActive] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [showMap, setShowMap] = useState<Record<string, boolean>>({});
|
||||
|
||||
// SSH 字段 // Logan.Li
|
||||
const [sshUser, setSshUser] = useState('root');
|
||||
const [sshPort, setSshPort] = useState('22');
|
||||
const [sshPassword, setSshPassword] = useState('');
|
||||
const [sshPrivateKey, setSshPrivateKey] = useState('');
|
||||
const [sshPublicKey, setSshPublicKey] = useState('');
|
||||
// Docker 字段
|
||||
const [dockerServer, setDockerServer] = useState('');
|
||||
const [dockerUsername, setDockerUsername] = useState('');
|
||||
const [dockerPassword, setDockerPassword] = useState('');
|
||||
// TLS 字段
|
||||
const [tlsCert, setTlsCert] = useState('');
|
||||
const [tlsKey, setTlsKey] = useState('');
|
||||
const [tlsCa, setTlsCa] = useState('');
|
||||
// AKSK 字段
|
||||
const [, setAkskProvider] = useState('');
|
||||
const [akskAccessKey, setAkskAccessKey] = useState('');
|
||||
const [akskSecretKey, setAkskSecretKey] = useState('');
|
||||
const [, setAkskRegion] = useState('');
|
||||
// 数据库字段
|
||||
const [dbType, setDbType] = useState('mysql');
|
||||
const [dbHost, setDbHost] = useState('');
|
||||
const [dbPort, setDbPort] = useState('3306');
|
||||
const [dbName, setDbName] = useState('');
|
||||
const [dbUsername, setDbUsername] = useState('');
|
||||
const [dbPassword, setDbPassword] = useState('');
|
||||
// API 字段
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [apiEndpoint, setApiEndpoint] = useState('');
|
||||
// 通用字段
|
||||
const [genericKey, setGenericKey] = useState('');
|
||||
const [genericValue, setGenericValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (editData) {
|
||||
setName(editData.name);
|
||||
setVarName(editData.variable_name || '');
|
||||
setSecretType(editData.secret_type);
|
||||
setDescription(editData.description || '');
|
||||
setTags((editData.tags || []).join(', '));
|
||||
setIsActive(editData.is_active);
|
||||
// 编辑时不预填类型字段,仅修改用户填写的项 // Logan.Li
|
||||
setSshUser(''); setSshPort('22'); setSshPassword(''); setSshPrivateKey(''); setSshPublicKey('');
|
||||
setDockerServer(''); setDockerUsername(''); setDockerPassword('');
|
||||
setTlsCert(''); setTlsKey(''); setTlsCa('');
|
||||
setAkskProvider(''); setAkskAccessKey(''); setAkskSecretKey(''); setAkskRegion('');
|
||||
setDbType('mysql'); setDbHost(''); setDbPort('3306'); setDbName(''); setDbUsername(''); setDbPassword('');
|
||||
setApiKey(''); setApiEndpoint('');
|
||||
setGenericKey(''); setGenericValue('');
|
||||
} else {
|
||||
setName(''); setVarName(''); setSecretType('ssh'); setDescription(''); setTags(''); setIsActive(true);
|
||||
setSshUser('root'); setSshPort('22'); setSshPassword(''); setSshPrivateKey(''); setSshPublicKey('');
|
||||
setDockerServer(''); setDockerUsername(''); setDockerPassword('');
|
||||
setTlsCert(''); setTlsKey(''); setTlsCa('');
|
||||
setAkskProvider(''); setAkskAccessKey(''); setAkskSecretKey(''); setAkskRegion('');
|
||||
setDbType('mysql'); setDbHost(''); setDbPort('3306'); setDbName(''); setDbUsername(''); setDbPassword('');
|
||||
setApiKey(''); setApiEndpoint('');
|
||||
setGenericKey(''); setGenericValue('');
|
||||
}
|
||||
setShowMap({});
|
||||
}, [editData, open]);
|
||||
|
||||
const toggleShow = (key: string) => setShowMap(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) { toast.error('请填写凭据名称'); return; }
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
name: name.trim(),
|
||||
secret_type: secretType,
|
||||
description: description.trim(),
|
||||
tags: tags.split(',').map(t => t.trim()).filter(Boolean),
|
||||
is_active: isActive,
|
||||
};
|
||||
if (varName.trim()) payload.variable_name = varName.trim();
|
||||
|
||||
if (secretType === 'ssh') {
|
||||
if (sshUser.trim()) payload.ssh_user = sshUser.trim();
|
||||
if (sshPort) payload.ssh_port = parseInt(sshPort) || 22;
|
||||
if (sshPassword.trim()) payload.ssh_password = sshPassword.trim();
|
||||
if (sshPrivateKey.trim()) payload.ssh_private_key = sshPrivateKey.trim();
|
||||
if (sshPublicKey.trim()) payload.ssh_public_key = sshPublicKey.trim();
|
||||
} else if (secretType === 'docker') {
|
||||
if (dockerServer.trim()) payload.docker_server = dockerServer.trim();
|
||||
if (dockerUsername.trim()) payload.docker_username = dockerUsername.trim();
|
||||
if (dockerPassword.trim()) payload.docker_password = dockerPassword.trim();
|
||||
} else if (secretType === 'tls') {
|
||||
if (tlsCert.trim()) payload.tls_cert = tlsCert.trim();
|
||||
if (tlsKey.trim()) payload.tls_key = tlsKey.trim();
|
||||
if (tlsCa.trim()) payload.tls_ca = tlsCa.trim();
|
||||
} else if (secretType === 'aksk') {
|
||||
if (akskAccessKey.trim()) payload.aksk_access_key = akskAccessKey.trim();
|
||||
if (akskSecretKey.trim()) payload.aksk_secret_key = akskSecretKey.trim();
|
||||
} else if (secretType === 'database') {
|
||||
if (dbType.trim()) payload.db_type = dbType.trim();
|
||||
if (dbHost.trim()) payload.db_host = dbHost.trim();
|
||||
if (dbPort) payload.db_port = parseInt(dbPort) || 3306;
|
||||
if (dbName.trim()) payload.db_name = dbName.trim();
|
||||
if (dbUsername.trim()) payload.db_username = dbUsername.trim();
|
||||
if (dbPassword.trim()) payload.db_password = dbPassword.trim();
|
||||
} else if (secretType === 'api') {
|
||||
if (apiKey.trim()) payload.api_key = apiKey.trim();
|
||||
if (apiEndpoint.trim()) payload.api_endpoint = apiEndpoint.trim();
|
||||
} else if (secretType === 'generic') {
|
||||
if (genericKey.trim()) payload.generic_key = genericKey.trim();
|
||||
if (genericValue.trim()) payload.generic_value = genericValue.trim();
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
await credentialApi.update(editData!.id, payload);
|
||||
toast.success('已更新');
|
||||
} else {
|
||||
await credentialApi.create(payload);
|
||||
toast.success('已创建');
|
||||
}
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { message?: string } } };
|
||||
toast.error(err?.response?.data?.message || '操作失败');
|
||||
} finally { setSubmitting(false); }
|
||||
};
|
||||
|
||||
const FieldWithToggle = ({ label, value, onChange, placeholder, fieldKey, required, isLong }: {
|
||||
label: string; value: string; onChange: (v: string) => void; placeholder: string;
|
||||
fieldKey: string; required?: boolean; isLong?: boolean;
|
||||
}) => {
|
||||
const show = showMap[fieldKey];
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{label}{required && !isEdit && <span className="text-red-400"> *</span>}</Label>
|
||||
<div className="relative">
|
||||
{isLong ? (
|
||||
<textarea value={value} onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder} rows={5}
|
||||
className="w-full rounded-md bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] p-2 text-sm font-mono placeholder:text-[var(--dg-text-muted)]" />
|
||||
) : (
|
||||
<Input value={value} onChange={e => onChange(e.target.value)}
|
||||
type={show ? 'text' : 'password'} placeholder={placeholder}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] pr-10 font-mono text-sm" />
|
||||
)}
|
||||
<button type="button" onClick={() => toggleShow(fieldKey)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] text-xs">
|
||||
{show ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TI = TYPE_CONFIG[secretType];
|
||||
const TIicon = TI?.icon || Key;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[620px] max-h-[90vh] overflow-y-auto bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<TIicon className={`h-5 w-5 ${TI?.color || ''}`} />
|
||||
{isEdit ? '编辑凭据' : '新增凭据'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)] text-xs">
|
||||
{TI?.desc || '管理敏感凭据信息'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
{/* 基本信息 // Logan.Li */}
|
||||
<div className="border-b border-[var(--dg-border)] pb-3">
|
||||
<div className="text-xs font-medium text-[var(--dg-text-secondary)] mb-2">基本信息</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">凭据名称 <span className="text-red-400">*</span></Label>
|
||||
<Input value={name} onChange={e => setName(e.target.value)} placeholder="如:生产环境 SSH"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">变量别名</Label>
|
||||
<Input value={varName} onChange={e => setVarName(e.target.value)} placeholder="如:MYSQL_PROD"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">类型{!isEdit && <span className="text-red-400"> *</span>}</Label>
|
||||
<Select value={secretType} onValueChange={(v) => setSecretType(v as SecretType)} disabled={isEdit}>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-[300px]">
|
||||
{(Object.entries(TYPE_CONFIG) as [SecretType, typeof TYPE_CONFIG[SecretType]][]).map(([k, v]) => (
|
||||
<SelectItem key={k} value={k}>
|
||||
<span className="flex items-center gap-2"><v.icon className="h-4 w-4" />{v.label}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">标签</Label>
|
||||
<Input value={tags} onChange={e => setTags(e.target.value)} placeholder="生产环境, 阿里云"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">描述</Label>
|
||||
<Input value={description} onChange={e => setDescription(e.target.value)} placeholder="可选备注信息"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 类型专属字段 // Logan.Li */}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-[var(--dg-text-secondary)] mb-2 flex items-center gap-2">
|
||||
<TIicon className="h-3.5 w-3.5" />{TI?.label || secretType} 配置
|
||||
{isEdit && <span className="text-[var(--dg-text-muted)]">(仅填写需要修改的字段)</span>}
|
||||
</div>
|
||||
|
||||
{secretType === 'ssh' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">SSH 用户</Label>
|
||||
<Input value={sshUser} onChange={e => setSshUser(e.target.value)} placeholder="root"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">SSH 端口</Label>
|
||||
<Input value={sshPort} onChange={e => setSshPort(e.target.value)} placeholder="22" type="number"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="SSH 密码" value={sshPassword} onChange={setSshPassword}
|
||||
placeholder="密码(或留空用密钥)" fieldKey="ssh_pwd" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="私钥内容" value={sshPrivateKey} onChange={setSshPrivateKey}
|
||||
placeholder="粘贴 SSH 私钥..." fieldKey="ssh_pk" isLong />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="公钥内容" value={sshPublicKey} onChange={setSshPublicKey}
|
||||
placeholder="粘贴 SSH 公钥..." fieldKey="ssh_pub" isLong />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'docker' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">Registry 地址 <span className="text-red-400">*</span></Label>
|
||||
<Input value={dockerServer} onChange={e => setDockerServer(e.target.value)} placeholder="registry.example.com"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">用户名</Label>
|
||||
<Input value={dockerUsername} onChange={e => setDockerUsername(e.target.value)} placeholder="admin"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div><FieldWithToggle label="密码" value={dockerPassword} onChange={setDockerPassword}
|
||||
placeholder="Registry 密码" fieldKey="docker_pwd" required /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'tls' && (
|
||||
<div className="space-y-3">
|
||||
<FieldWithToggle label="证书 (PEM)" value={tlsCert} onChange={setTlsCert}
|
||||
placeholder="-----BEGIN CERTIFICATE-----" fieldKey="tls_cert" isLong required />
|
||||
<FieldWithToggle label="私钥 (PEM)" value={tlsKey} onChange={setTlsKey}
|
||||
placeholder="-----BEGIN PRIVATE KEY-----" fieldKey="tls_key" isLong required />
|
||||
<FieldWithToggle label="CA 证书" value={tlsCa} onChange={setTlsCa}
|
||||
placeholder="CA 证书内容..." fieldKey="tls_ca" isLong />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'aksk' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">Access Key ID <span className="text-red-400">*</span></Label>
|
||||
<Input value={akskAccessKey} onChange={e => setAkskAccessKey(e.target.value)} placeholder="AKIDxxxx..."
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="Secret Access Key" value={akskSecretKey} onChange={setAkskSecretKey}
|
||||
placeholder="输入 Secret Key" fieldKey="aksk_sk" required />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'database' && (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">数据库类型</Label>
|
||||
<Select value={dbType} onValueChange={setDbType}>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="mysql">MySQL</SelectItem>
|
||||
<SelectItem value="postgres">PostgreSQL</SelectItem>
|
||||
<SelectItem value="mongodb">MongoDB</SelectItem>
|
||||
<SelectItem value="redis">Redis</SelectItem>
|
||||
<SelectItem value="mssql">SQL Server</SelectItem>
|
||||
<SelectItem value="oracle">Oracle</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">主机 <span className="text-red-400">*</span></Label>
|
||||
<Input value={dbHost} onChange={e => setDbHost(e.target.value)} placeholder="10.0.0.1"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">端口</Label>
|
||||
<Input value={dbPort} onChange={e => setDbPort(e.target.value)} placeholder="3306" type="number"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">数据库名</Label>
|
||||
<Input value={dbName} onChange={e => setDbName(e.target.value)} placeholder="aiops"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">用户名</Label>
|
||||
<Input value={dbUsername} onChange={e => setDbUsername(e.target.value)} placeholder="root"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div><FieldWithToggle label="密码" value={dbPassword} onChange={setDbPassword}
|
||||
placeholder="数据库密码" fieldKey="db_pwd" /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'api' && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">API 端点</Label>
|
||||
<Input value={apiEndpoint} onChange={e => setApiEndpoint(e.target.value)} placeholder="https://api.example.com"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<FieldWithToggle label="API Key / Token" value={apiKey} onChange={setApiKey}
|
||||
placeholder="sk-xxxx..." fieldKey="api_key" required />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'generic' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">Key <span className="text-red-400">*</span></Label>
|
||||
<Input value={genericKey} onChange={e => setGenericKey(e.target.value)} placeholder="MY_SECRET"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<FieldWithToggle label="Value" value={genericValue} onChange={setGenericValue}
|
||||
placeholder="密钥值" fieldKey="generic_val" required />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>取消</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting} className="bg-[var(--dg-blue)] hover:bg-[var(--dg-blue)]/80">
|
||||
{submitting ? '保存中...' : (isEdit ? '更新' : '创建')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user