style: refine Root CA Management layout and restore spec-sync buttons

This commit is contained in:
dyzulk
2026-01-07 04:58:44 +07:00
parent fc04d0112f
commit 713fab3fba
5 changed files with 144 additions and 99 deletions

View File

@@ -53,7 +53,7 @@ export default function RootCaManagementClient() {
const { addToast } = useToast(); const { addToast } = useToast();
const { data, error, mutate, isLoading } = useSWR("/api/admin/ca-certificates", fetcher); const { data, error, mutate, isLoading } = useSWR("/api/admin/ca-certificates", fetcher);
const [isRenewing, setIsRenewing] = useState(false); const [isRenewing, setIsRenewing] = useState(false);
const [isSyncing, setIsSyncing] = useState(false); const [activeSync, setActiveSync] = useState<string | null>(null);
const [isPromoting, setIsPromoting] = useState(false); const [isPromoting, setIsPromoting] = useState(false);
const [confirmRenewUuid, setConfirmRenewUuid] = useState<string | null>(null); const [confirmRenewUuid, setConfirmRenewUuid] = useState<string | null>(null);
@@ -83,7 +83,8 @@ export default function RootCaManagementClient() {
}; };
const handleSyncCdn = async (type: 'all' | 'crt' | 'installers' | 'bundles', mode: 'latest' | 'archive' | 'both' = 'both') => { const handleSyncCdn = async (type: 'all' | 'crt' | 'installers' | 'bundles', mode: 'latest' | 'archive' | 'both' = 'both') => {
setIsSyncing(true); const syncKey = `${type}-${mode}`;
setActiveSync(syncKey);
try { try {
let endpoint = "/api/admin/ca-certificates/sync-cdn"; let endpoint = "/api/admin/ca-certificates/sync-cdn";
let msg = `Sync successful (Mode: ${mode})`; let msg = `Sync successful (Mode: ${mode})`;
@@ -106,7 +107,7 @@ export default function RootCaManagementClient() {
console.error(err); console.error(err);
addToast(err.response?.data?.message || "Sync failed", "error"); addToast(err.response?.data?.message || "Sync failed", "error");
} finally { } finally {
setIsSyncing(false); setActiveSync(null);
} }
}; };
@@ -129,61 +130,58 @@ export default function RootCaManagementClient() {
const certificates = data?.data || []; const certificates = data?.data || [];
return ( return (
<div> <div className="space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-4 gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between mb-2 gap-4">
<PageBreadcrumb pageTitle={t("management_title")} /> <PageBreadcrumb pageTitle={t("management_title")} />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="space-y-6">
{/* Left Column: Management & Renew Table */} {/* Main CA Management Table */}
<div className="lg:col-span-2 space-y-6"> <ComponentCard
<CdnManagementCard title={t("card_title")}
onSync={handleSyncCdn} desc={t("card_desc")}
isSyncing={isSyncing} className="relative"
disabled={isLoading} >
<RootCaTable
certificates={certificates.filter((c: any) => c.is_latest || certificates.length === 1)}
onRenew={setConfirmRenewUuid}
isRenewing={isRenewing}
/> />
<ComponentCard {(isLoading || isRenewing) && (
title={t("card_title")} <div className="absolute inset-0 bg-white/50 dark:bg-gray-900/50 flex items-center justify-center z-10 backdrop-blur-sm rounded-2xl">
desc={t("card_desc")} <PageLoader text={t("processing")} className="h-full" />
className="relative" </div>
> )}
<RootCaTable </ComponentCard>
certificates={certificates.filter((c: any) => c.is_latest || certificates.length === 1)}
onRenew={setConfirmRenewUuid}
isRenewing={isRenewing}
/>
{(isLoading || isRenewing) && ( {/* CDN Synchronization Controls */}
<div className="absolute inset-0 bg-white/50 dark:bg-gray-900/50 flex items-center justify-center z-10 backdrop-blur-sm rounded-2xl"> <CdnManagementCard
<PageLoader text={t("processing")} className="h-full" /> onSync={handleSyncCdn}
</div> activeSync={activeSync}
)} disabled={isLoading || isPromoting}
</ComponentCard> />
<div className="p-4 bg-blue-50 border-l-4 border-blue-400 dark:bg-blue-900/20 dark:border-blue-600 rounded-md"> {/* Full-width Archive History */}
<h4 className="text-sm font-bold text-blue-800 dark:text-blue-200">{t("info_title")}</h4> <ComponentCard
<ul className="mt-2 text-xs text-blue-700 dark:text-blue-300 list-disc list-inside space-y-1"> title="Version Archives"
<li>{t("info_point_1")}</li> desc="Browse historical versions and promote them back to Latest if needed. This table provides full visibility into your CDN audit trail."
<li>{t("info_point_2")}</li> >
<li>{t("info_point_3")}</li> <ArchiveManagementTable
<li>{t("info_point_4")}</li> certificates={certificates}
</ul> onPromote={handlePromote}
</div> isPromoting={isPromoting}
</div> />
</ComponentCard>
{/* Right Column: Archives */} <div className="p-4 bg-blue-50 border-l-4 border-blue-400 dark:bg-blue-900/20 dark:border-blue-600 rounded-md">
<div className="lg:col-span-1"> <h4 className="text-sm font-bold text-blue-800 dark:text-blue-200">{t("info_title")}</h4>
<ComponentCard <ul className="mt-2 text-xs text-blue-700 dark:text-blue-300 list-disc list-inside space-y-1">
title="Version Archives" <li>{t("info_point_1")}</li>
desc="Browse historical versions and promote them back to Latest if needed." <li>{t("info_point_2")}</li>
> <li>{t("info_point_3")}</li>
<ArchiveManagementTable <li>{t("info_point_4")}</li>
certificates={certificates} </ul>
onPromote={handlePromote}
isPromoting={isPromoting}
/>
</ComponentCard>
</div> </div>
</div> </div>

View File

@@ -6,54 +6,61 @@ import { useTranslations } from "next-intl";
interface CdnManagementCardProps { interface CdnManagementCardProps {
onSync: (type: "all" | "crt" | "installers" | "bundles", mode: "latest" | "archive" | "both") => void; onSync: (type: "all" | "crt" | "installers" | "bundles", mode: "latest" | "archive" | "both") => void;
isSyncing: boolean; activeSync: string | null;
disabled: boolean; disabled: boolean;
} }
export default function CdnManagementCard({ onSync, isSyncing, disabled }: CdnManagementCardProps) { export default function CdnManagementCard({ onSync, activeSync, disabled }: CdnManagementCardProps) {
const t = useTranslations("RootCA"); const t = useTranslations("RootCA");
const SyncButton = ({ const SyncButton = ({
onClick, type,
mode,
label, label,
desc, desc,
variant = "blue" variant = "blue"
}: { }: {
onClick: () => void; type: "all" | "crt" | "installers" | "bundles";
mode: "latest" | "archive" | "both";
label: string; label: string;
desc: string; desc: string;
variant?: "blue" | "purple" | "indigo" | "gray" variant?: "blue" | "purple" | "indigo" | "gray" | "success" | "warning";
}) => { }) => {
const syncKey = `${type}-${mode}`;
const isLoading = activeSync === syncKey;
const variantClasses = { const variantClasses = {
blue: "bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400", blue: "bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400",
purple: "bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400", purple: "bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400",
indigo: "bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400", indigo: "bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400",
gray: "bg-gray-600 hover:bg-gray-700 disabled:bg-gray-400", gray: "bg-gray-600 hover:bg-gray-700 disabled:bg-gray-400",
success: "bg-success-600 hover:bg-success-700 disabled:bg-success-400",
warning: "bg-warning-500 hover:bg-warning-600 disabled:bg-warning-400",
}; };
return ( return (
<div className="flex flex-col gap-2 p-4 border border-gray-100 dark:border-white/[0.05] rounded-xl hover:bg-gray-50 dark:hover:bg-white/[0.02] transition-colors"> <div className="flex flex-col gap-2 p-4 border border-gray-100 dark:border-white/[0.05] rounded-xl hover:bg-gray-50 dark:hover:bg-white/[0.02] transition-colors h-full">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4 h-full">
<div className="flex flex-col"> <div className="flex flex-col flex-1">
<span className="text-sm font-bold text-gray-800 dark:text-white/90">{label}</span> <span className="text-sm font-bold text-gray-800 dark:text-white/90">{label}</span>
<p className="text-xs text-gray-500 dark:text-gray-400 max-w-[250px]">{desc}</p> <p className="text-[11px] text-gray-500 dark:text-gray-400 leading-tight mt-1">{desc}</p>
</div> </div>
<button <button
onClick={onClick} onClick={() => onSync(type, mode)}
disabled={disabled || isSyncing} disabled={disabled || activeSync !== null}
className={`flex items-center justify-center gap-2 px-4 py-2 ${variantClasses[variant]} text-white text-xs font-medium rounded-lg transition-all shadow-sm active:scale-95 whitespace-nowrap`} className={`flex items-center justify-center gap-2 px-4 py-2 ${variantClasses[variant]} text-white text-[11px] font-medium rounded-lg transition-all shadow-sm active:scale-95 whitespace-nowrap min-w-[100px]`}
> >
{isSyncing ? ( {isLoading ? (
<svg className="animate-spin h-3.5 w-3.5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg className="animate-spin h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
) : ( ) : (
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg> </svg>
)} )}
{isSyncing ? "Syncing..." : "Sync Now"} {isLoading ? "Syncing..." : "Sync Now"}
</button> </button>
</div> </div>
</div> </div>
@@ -61,36 +68,68 @@ export default function CdnManagementCard({ onSync, isSyncing, disabled }: CdnMa
}; };
return ( return (
<ComponentCard <div className="space-y-4">
title="CDN Management" <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
desc="Control how your SSL assets are distributed and archived on the Global CDN." {/* Specific Assets Column */}
> <ComponentCard
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> title={t("cdn_group_assets")}
<SyncButton desc={t("cdn_group_assets_desc")}
label="Sync All (Latest & Archive)" >
desc="The safest option. Updates human-friendly URLs and creates permanent archives for all certs." <div className="grid grid-cols-1 gap-4">
onClick={() => onSync("all", "both")} <SyncButton
variant="indigo" type="crt"
/> mode="both"
<SyncButton label="Update CRT Only"
label="Update Public Link (Latest Only)" desc="Hanya unggah file sertifikat publik (.crt, .der)."
desc="Only updates 'Clean URLs'. Useful for quickly updating current public certificates." variant="blue"
onClick={() => onSync("all", "latest")} />
variant="blue" <SyncButton
/> type="installers"
<SyncButton mode="both"
label="Create Archives (Archive Only)" label="Update Scripts Only"
desc="Only uploads to versioned folders. Won't affect current public download links." desc="Hanya perbarui skrip installer individual (.sh, .bat, .mac)."
onClick={() => onSync("all", "archive")} variant="purple"
variant="purple" />
/> <SyncButton
<SyncButton type="bundles"
label="Sync Global Bundles" mode="latest"
desc="Regenerate all-in-one installers using currently promoted 'Latest' versions." label="Update Global Bundles"
onClick={() => onSync("bundles", "latest")} desc="Perbarui paket installer sapujagat (Recommended)."
variant="gray" variant="success"
/> />
</div>
</ComponentCard>
{/* Sync Modes Column */}
<ComponentCard
title={t("cdn_group_modes")}
desc={t("cdn_group_modes_desc")}
>
<div className="grid grid-cols-1 gap-4">
<SyncButton
type="all"
mode="both"
label="Dual Sync (Latest & Archive)"
desc="Sinkronisasi massal ke kedua jalur. Paling aman untuk integritas data."
variant="indigo"
/>
<SyncButton
type="all"
mode="latest"
label="Latest Sync (Clean URLs)"
desc="Hanya perbarui link publik utama pendukung installer."
variant="blue"
/>
<SyncButton
type="all"
mode="archive"
label="Archive Sync (Versioned)"
desc="Hanya simpan arsip permanen tanpa mengubah link publik."
variant="gray"
/>
</div>
</ComponentCard>
</div> </div>
</ComponentCard> </div>
); );
} }

View File

@@ -109,7 +109,7 @@ export default function RootCaTable({
<TableCell className="px-5 py-4 text-start text-theme-xs text-gray-500 dark:text-gray-400"> <TableCell className="px-5 py-4 text-start text-theme-xs text-gray-500 dark:text-gray-400">
<div className="flex flex-col"> <div className="flex flex-col">
<span>{new Date(cert.valid_from).toLocaleString()}</span> <span>{new Date(cert.valid_from).toLocaleString()}</span>
<span className="text-gray-400">{t("to")}</span> <span className="text-gray-400">{t("validity_to_label")}</span>
<span>{new Date(cert.valid_to).toLocaleString()}</span> <span>{new Date(cert.valid_to).toLocaleString()}</span>
</div> </div>
</TableCell> </TableCell>

View File

@@ -639,6 +639,10 @@
"management_title": "Root CA Management", "management_title": "Root CA Management",
"card_title": "CA Certificates", "card_title": "CA Certificates",
"card_desc": "Manage Root and Intermediate Certification Authorities", "card_desc": "Manage Root and Intermediate Certification Authorities",
"cdn_group_assets": "Sync Specific Assets",
"cdn_group_assets_desc": "Update specific file types to the CDN (Uses 'Both' mode by default).",
"cdn_group_modes": "Dual CDN Strategy",
"cdn_group_modes_desc": "Control asset distribution paths (Clean URLs vs Global Archives).",
"toast_renew_success": "CA Certificate renewed successfully.", "toast_renew_success": "CA Certificate renewed successfully.",
"toast_renew_failed": "Failed to renew CA certificate", "toast_renew_failed": "Failed to renew CA certificate",
"load_failed": "Failed to load CA certificates. Admin access required.", "load_failed": "Failed to load CA certificates. Admin access required.",
@@ -661,7 +665,7 @@
"validity_to_label": "to", "validity_to_label": "to",
"status_valid": "Valid", "status_valid": "Valid",
"status_expired": "Expired", "status_expired": "Expired",
"renew_button": "Renew (10Y)", "renew_button": "Renew Now",
"no_ca_search": "No CAs matched \"{term}\"", "no_ca_search": "No CAs matched \"{term}\"",
"no_ca_found": "No Root CA certificates found. Run CA Setup from Certificates page." "no_ca_found": "No Root CA certificates found. Run CA Setup from Certificates page."
}, },

View File

@@ -639,6 +639,10 @@
"management_title": "Manajemen Root CA", "management_title": "Manajemen Root CA",
"card_title": "Sertifikat CA", "card_title": "Sertifikat CA",
"card_desc": "Kelola Otoritas Sertifikasi Root dan Intermediat", "card_desc": "Kelola Otoritas Sertifikasi Root dan Intermediat",
"cdn_group_assets": "Aset Sinkronisasi (Asset-specific)",
"cdn_group_assets_desc": "Perbarui file spesifik ke CDN (Menggunakan mode 'Both' secara default).",
"cdn_group_modes": "Strategi Dual CDN (Mode-specific)",
"cdn_group_modes_desc": "Kontrol jalur distribusi aset (Clean URLs vs Global Archives).",
"toast_renew_success": "Sertifikat CA berhasil diperbarui.", "toast_renew_success": "Sertifikat CA berhasil diperbarui.",
"toast_renew_failed": "Gagal memperbarui sertifikat CA", "toast_renew_failed": "Gagal memperbarui sertifikat CA",
"load_failed": "Gagal memuat sertifikat CA. Diperlukan akses admin.", "load_failed": "Gagal memuat sertifikat CA. Diperlukan akses admin.",
@@ -661,7 +665,7 @@
"validity_to_label": "ke", "validity_to_label": "ke",
"status_valid": "Valid", "status_valid": "Valid",
"status_expired": "Kedaluwarsa", "status_expired": "Kedaluwarsa",
"renew_button": "Perbarui (10T)", "renew_button": "Perbarui",
"no_ca_search": "Tidak ada CA yang cocok dengan \"{term}\"", "no_ca_search": "Tidak ada CA yang cocok dengan \"{term}\"",
"no_ca_found": "Sertifikat Root CA tidak ditemukan. Jalankan Pengaturan CA dari halaman Sertifikat." "no_ca_found": "Sertifikat Root CA tidak ditemukan. Jalankan Pengaturan CA dari halaman Sertifikat."
}, },