115 lines
5.3 KiB
TypeScript
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>
|
|
);
|
|
}
|