Files
aiops/frontend/src/components/kubernetes/resource-visual-editors/SecretEditor.tsx
T

115 lines
5.3 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
// SecretEditor — Secret 键值对表格可视化编辑器 (Base64 编码/解码, 从 ResourceVisualEditor.tsx 拆分)
import { useState } from 'react';
import { Plus, Trash2, Loader2, Save } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import i18n from '@/i18n';
export interface SecretEditorProps {
manifest: any;
onSave: (m: any) => void;
saving: boolean;
isCreate?: boolean;
}
export function SecretEditor({ manifest, onSave, saving, isCreate }: SecretEditorProps) {
const rawData = manifest?.data || {};
const [resName, setResName] = useState(manifest?.metadata?.name || '');
const [expandAll, setExpandAll] = useState(false);
// Decode base64 values for editing
const [entries, setEntries] = useState<{ key: string; value: string; encoded: boolean }[]>(
Object.entries(rawData).map(([key, value]) => {
const v = String(value);
try {
// Try to decode — if it's valid base64
const decoded = atob(v);
return { key, value: decoded, encoded: true };
} catch {
return { key, value: v, encoded: false };
}
})
);
const update = (i: number, field: 'key' | 'value', val: string) => {
const next = [...entries];
next[i] = { ...next[i], [field]: val };
setEntries(next);
};
const addRow = () => setEntries([...entries, { key: '', value: '', encoded: true }]);
const removeRow = (i: number) => setEntries(entries.filter((_, idx) => idx !== i));
const handleSave = () => {
const newData: Record<string, string> = {};
entries.forEach(e => {
if (e.key.trim()) {
// Re-encode if originally base64
newData[e.key.trim()] = e.encoded ? btoa(e.value) : e.value;
}
});
onSave({ ...manifest, metadata: { ...manifest.metadata, name: resName.trim() }, data: newData });
};
return (
<div className="flex flex-col h-full">
<div className="shrink-0 grid grid-cols-2 gap-4">
<div>
<Label className="text-[var(--dg-text-secondary)] text-xs"> {i18n.t('kubernetes.secret.secretName')} </Label>
<Input value={resName} onChange={(e) => setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
</div>
<div>
<Label className="text-[var(--dg-text-secondary)] text-xs"> {i18n.t('system.credentialType')} </Label>
<Input value={manifest?.type || 'Opaque'} readOnly className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
</div>
</div>
<div className="flex-1 min-h-0 flex flex-col mt-3">
<div className="flex items-center justify-between mb-2 shrink-0">
<Label className="text-[var(--dg-text-secondary)] text-sm"> {i18n.t('common.loading')} </Label>
<div className="flex gap-1">
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => setExpandAll(!expandAll)}>
{expandAll ? i18n.t('k8s.configMap.collapseAll') : i18n.t('system.expandAll')}
</Button>
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={addRow}><Plus className="h-3 w-3 mr-1" /> {i18n.t('monitor.dashboard.custom.addBtn')} </Button>
</div>
</div>
<div className="flex-1 min-h-0 overflow-y-auto pr-1 space-y-2 border border-[var(--dg-border)] rounded-lg bg-[var(--dg-bg-secondary)] p-2">
{entries.map((entry, i) => (
<div key={i} className="flex items-start gap-2 p-2 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
<Input
value={entry.key}
onChange={e => update(i, 'key', e.target.value)}
placeholder={i18n.t('k8s.rve.keyPlaceholder')}
className="flex-1 font-mono text-xs h-8 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
/>
<Textarea
value={entry.value}
onChange={e => update(i, 'value', e.target.value)}
placeholder={i18n.t('k8s.rve.keyPlaceholder')}
rows={expandAll || entry.value.length > 300 ? 10 : (entry.value.length > 100 ? 6 : 3)}
className="flex-[2] font-mono text-xs min-h-[70px] max-h-[420px] resize-y bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] leading-relaxed"
/>
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-red hover:dg-text-red shrink-0" onClick={() => removeRow(i)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
))}
{entries.length === 0 && (
<div className="text-center py-8 text-[var(--dg-text-muted)] text-xs"> {i18n.t('common.name')} </div>
)}
</div>
</div>
<div className="shrink-0 flex justify-end gap-2 pt-3 mt-2 border-t border-[var(--dg-border)]">
<Button size="sm" className="bg-blue-600 hover:bg-blue-700" onClick={handleSave} disabled={saving}>
{saving ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Save className="h-3.5 w-3.5 mr-1" />}
保存密钥
</Button>
</div>
</div>
);
}