333 lines
13 KiB
TypeScript
333 lines
13 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
ShieldCheck,
|
|
Key,
|
|
FileCode,
|
|
CheckCircle2,
|
|
Plus,
|
|
Clock,
|
|
GitCommit,
|
|
Save,
|
|
Lock,
|
|
Code,
|
|
} from "lucide-react";
|
|
import { CredentialRef, AuditLogEntry, SiteConfig } from "../types";
|
|
|
|
interface ConfigAndAuditTabProps {
|
|
credentials: CredentialRef[];
|
|
auditLogs: AuditLogEntry[];
|
|
siteConfig: SiteConfig;
|
|
onAddCredential: (
|
|
name: string,
|
|
type: CredentialRef["type"],
|
|
val: string,
|
|
) => Promise<void>;
|
|
onSaveSiteConfig: (rawYaml: string) => Promise<void>;
|
|
}
|
|
|
|
export const ConfigAndAuditTab: React.FC<ConfigAndAuditTabProps> = ({
|
|
credentials,
|
|
auditLogs,
|
|
siteConfig,
|
|
onAddCredential,
|
|
onSaveSiteConfig,
|
|
}) => {
|
|
const [activeSubTab, setActiveSubTab] = useState<
|
|
"credentials" | "audit" | "yaml"
|
|
>("yaml");
|
|
const [yamlContent, setYamlContent] = useState(siteConfig.rawYaml);
|
|
const [yamlSavedMsg, setYamlSavedMsg] = useState<string | null>(null);
|
|
|
|
// New Credential Form
|
|
const [showAddCredModal, setShowAddCredModal] = useState(false);
|
|
const [credName, setCredName] = useState("");
|
|
const [credType, setCredType] = useState<CredentialRef["type"]>("ssh_key");
|
|
const [credVal, setCredVal] = useState("");
|
|
|
|
const handleSaveYaml = async () => {
|
|
try {
|
|
await onSaveSiteConfig(yamlContent);
|
|
setYamlSavedMsg(
|
|
"Updated site.yaml successfully! Created Git commit on site-definition repository.",
|
|
);
|
|
setTimeout(() => setYamlSavedMsg(null), 4000);
|
|
} catch (err: any) {
|
|
alert(`YAML Save Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const handleCreateCred = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!credName || !credVal) return;
|
|
await onAddCredential(credName, credType, credVal);
|
|
setShowAddCredModal(false);
|
|
setCredName("");
|
|
setCredVal("");
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* HEADER */}
|
|
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center space-x-2">
|
|
<h1 className="text-xl font-serif italic text-white">
|
|
System Settings & Audit
|
|
</h1>
|
|
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-emerald-950/40 text-emerald-400 border border-emerald-500/20">
|
|
Control Plane Configuration
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-neutral-400 mt-1">
|
|
Manage global site.yaml settings, secret credential references, and
|
|
view system audit logs.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SUB-TAB NAV */}
|
|
<div className="flex border-b border-[#1F1F1F] space-x-6 text-xs font-semibold uppercase tracking-wider">
|
|
<button
|
|
onClick={() => setActiveSubTab("yaml")}
|
|
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 ${
|
|
activeSubTab === "yaml"
|
|
? "border-[#C5A059] text-[#C5A059]"
|
|
: "border-transparent text-neutral-500 hover:text-neutral-300"
|
|
}`}
|
|
>
|
|
<FileCode className="w-4 h-4" />
|
|
<span>Declarative site.yaml (Git Commits)</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveSubTab("credentials")}
|
|
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 ${
|
|
activeSubTab === "credentials"
|
|
? "border-[#C5A059] text-[#C5A059]"
|
|
: "border-transparent text-neutral-500 hover:text-neutral-300"
|
|
}`}
|
|
>
|
|
<Key className="w-4 h-4" />
|
|
<span>Secret Credential References ({credentials.length})</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveSubTab("audit")}
|
|
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 ${
|
|
activeSubTab === "audit"
|
|
? "border-[#C5A059] text-[#C5A059]"
|
|
: "border-transparent text-neutral-500 hover:text-neutral-300"
|
|
}`}
|
|
>
|
|
<Clock className="w-4 h-4" />
|
|
<span>Audit Trail Log ({auditLogs.length})</span>
|
|
</button>
|
|
</div>
|
|
|
|
{activeSubTab === "yaml" ? (
|
|
<div className="space-y-4">
|
|
{yamlSavedMsg && (
|
|
<div className="p-4 bg-emerald-950/40 border border-emerald-500/20 text-emerald-300 text-xs rounded-sm flex items-center space-x-2">
|
|
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
|
<span>{yamlSavedMsg}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-[#111111] rounded-sm p-6 border border-[#1F1F1F] space-y-4">
|
|
<div className="flex items-center justify-between text-white border-b border-[#1F1F1F] pb-3">
|
|
<div className="flex items-center space-x-2">
|
|
<Code className="w-5 h-5 text-[#C5A059]" />
|
|
<span className="font-mono font-bold text-sm">/site.yaml</span>
|
|
<span className="text-xs text-neutral-400">
|
|
Canonical Protocol Model
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleSaveYaml}
|
|
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black rounded-sm text-xs font-semibold shadow-sm transition-all flex items-center space-x-1.5 uppercase tracking-wider"
|
|
>
|
|
<GitCommit className="w-3.5 h-3.5" />
|
|
<span>Save & Commit to Git</span>
|
|
</button>
|
|
</div>
|
|
|
|
<textarea
|
|
rows={16}
|
|
value={yamlContent}
|
|
onChange={(e) => setYamlContent(e.target.value)}
|
|
className="w-full bg-[#0A0A0A] text-[#C5A059] font-mono text-xs p-4 rounded-sm border border-[#1F1F1F] focus:outline-none focus:border-[#C5A059]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : activeSubTab === "credentials" ? (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">
|
|
Secret Reference Vault
|
|
</span>
|
|
<button
|
|
onClick={() => setShowAddCredModal(true)}
|
|
className="px-3.5 py-1.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black rounded-sm text-xs font-semibold flex items-center space-x-1 uppercase tracking-wider"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Store Secret Reference</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs font-mono">
|
|
{credentials.map((c) => (
|
|
<div
|
|
key={c.id}
|
|
className="p-4 bg-[#111111] rounded-sm border border-[#1F1F1F] shadow-xs space-y-2"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-bold text-white text-sm truncate">
|
|
{c.name}
|
|
</span>
|
|
<Key className="w-4 h-4 text-[#C5A059] shrink-0" />
|
|
</div>
|
|
<div className="text-[11px] text-neutral-400 space-y-1">
|
|
<div>
|
|
Reference ID:{" "}
|
|
<code className="text-[#C5A059] font-bold">{c.id}</code>
|
|
</div>
|
|
<div>
|
|
Type:{" "}
|
|
<span className="uppercase text-neutral-500">{c.type}</span>
|
|
</div>
|
|
<div className="p-2 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] text-neutral-300 truncate font-mono text-[10px]">
|
|
{c.maskedValue}
|
|
</div>
|
|
<div className="pt-2 text-neutral-500 text-[10px] flex items-center justify-between">
|
|
<span>Used {c.usageCount} times</span>
|
|
<span>
|
|
Created: {new Date(c.createdAt).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* AUDIT LOG TRAIL */
|
|
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
|
|
<h2 className="text-base font-bold text-white">
|
|
System Audit Trail Log
|
|
</h2>
|
|
|
|
<div className="bg-[#0A0A0A] text-neutral-200 font-mono text-xs rounded-sm p-4 border border-[#1F1F1F] space-y-2 max-h-[500px] overflow-y-auto">
|
|
{auditLogs.map((log) => (
|
|
<div
|
|
key={log.id}
|
|
className="py-2 border-b border-[#141414] last:border-0 flex items-start justify-between"
|
|
>
|
|
<div>
|
|
<div className="flex items-center space-x-2">
|
|
<span className="text-[#C5A059] font-bold">
|
|
[{log.category.toUpperCase()}]
|
|
</span>
|
|
<span className="text-white font-semibold">
|
|
{log.action}
|
|
</span>
|
|
<span className="text-[10px] text-emerald-400 bg-emerald-950/40 border border-emerald-500/20 px-1.5 py-0.5 rounded-xs font-mono uppercase font-bold">
|
|
{log.result}
|
|
</span>
|
|
</div>
|
|
<p className="text-neutral-400 text-[11px] mt-1">
|
|
{log.details}
|
|
</p>
|
|
</div>
|
|
<span className="text-neutral-500 text-[10px]">
|
|
{new Date(log.timestamp).toLocaleTimeString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ADD CREDENTIAL MODAL */}
|
|
{showAddCredModal && (
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs z-50 flex items-center justify-center p-4">
|
|
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] max-w-md w-full p-6 shadow-2xl space-y-4">
|
|
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
|
|
<h3 className="text-base font-bold text-white">
|
|
Store New Secret Reference
|
|
</h3>
|
|
<button
|
|
onClick={() => setShowAddCredModal(false)}
|
|
className="text-neutral-500 hover:text-white text-xl leading-none"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleCreateCred} className="space-y-4 text-xs">
|
|
<div>
|
|
<label className="block font-semibold text-neutral-300 mb-1">
|
|
Secret Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="e.g. Gitea Internal Deploy Key"
|
|
value={credName}
|
|
onChange={(e) => setCredName(e.target.value)}
|
|
className="w-full px-3 py-2 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-white placeholder-neutral-600 focus:outline-none focus:border-[#C5A059]"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-semibold text-neutral-300 mb-1">
|
|
Type
|
|
</label>
|
|
<select
|
|
value={credType}
|
|
onChange={(e) => setCredType(e.target.value as any)}
|
|
className="w-full px-3 py-2 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-white font-mono focus:outline-none focus:border-[#C5A059]"
|
|
>
|
|
<option value="ssh_key">SSH Private Key</option>
|
|
<option value="token">OAuth / Deploy Token</option>
|
|
<option value="password">Password</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block font-semibold text-neutral-300 mb-1">
|
|
Secret Value (Encrypted at rest, never written to Git)
|
|
</label>
|
|
<textarea
|
|
rows={4}
|
|
required
|
|
placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ..."
|
|
value={credVal}
|
|
onChange={(e) => setCredVal(e.target.value)}
|
|
className="w-full px-3 py-2 bg-[#0A0A0A] border border-[#1F1F1F] rounded-sm text-white font-mono focus:outline-none focus:border-[#C5A059]"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end space-x-3 pt-3 border-t border-[#1F1F1F]">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAddCredModal(false)}
|
|
className="px-4 py-2 border border-[#222222] text-neutral-300 rounded-sm hover:bg-[#1A1A1A]"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-2 bg-[#C5A059] text-black font-semibold rounded-sm hover:bg-[#b38f4a] uppercase tracking-wider"
|
|
>
|
|
Save Secret Reference
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|