"use client"; import { useState } from "react"; import { Cloud, Database, HardDrive, Key, Loader2, Pencil, Plus, Power, TestTube2, CloudCheck, Trash2, X, type LucideIcon, } from "lucide-react"; import TablePagination from "@/components/TablePagination"; import { FieldLabel } from "@/app/settings/components/SettingsPrimitives"; import type { S3StorageDestinationInput, S3StorageDestinationRecord, S3StorageProvider, Server as ServerRecord, } from "@/lib/api"; interface S3StorageSettingsPanelProps { destinations: S3StorageDestinationRecord[]; servers: ServerRecord[]; actionLoading: string | null; savingDestinationId: string | null; onUpsertDestination: (destination: DestinationDraft) => Promise; onVerifyDestination: (destination: DestinationDraft) => Promise; onToggleDestination: ( destination: S3StorageDestinationRecord, ) => Promise; onDeleteDestination: (destination: S3StorageDestinationRecord) => void; } type DestinationDraft = S3StorageDestinationInput & { id?: string; hasSecretAccessKey: boolean; }; const PROVIDER_OPTIONS: Array<{ value: S3StorageProvider; label: string; description: string; icon: LucideIcon; color: string; }> = [ { value: "awsS3", label: "AWS S3", description: "Default Amazon S3 region endpoint for primary backups.", icon: Database, color: "#f59e0b", }, { value: "cloudflareR2", label: "Cloudflare R2", description: "S3-compatible bucket for edge-friendly object storage.", icon: Cloud, color: "#f97316", }, { value: "digitalOceanSpaces", label: "DO Spaces", description: "DigitalOcean object storage with custom endpoint support.", icon: HardDrive, color: "#2563eb", }, { value: "dreamObjects", label: "DreamObjects", description: "DreamHost S3-compatible destination for archives.", icon: Cloud, color: "#10b981", }, { value: "googleCloudStorage", label: "Google Cloud Storage", description: "Use GCS interoperability key for S3-compatible access.", icon: Database, color: "#0ea5e9", }, { value: "ibmCos", label: "IBM COS", description: "IBM Cloud Object Storage with region-aware endpoint.", icon: HardDrive, color: "#8b5cf6", }, { value: "alibabaOss", label: "Alibaba OSS", description: "OSS bucket for multi-region storage replication.", icon: Cloud, color: "#ef4444", }, { value: "huaweiObs", label: "Huawei OBS", description: "Object storage for Huawei Cloud workloads.", icon: Database, color: "#06b6d4", }, { value: "ceph", label: "Ceph", description: "Private S3-compatible cluster with custom endpoint.", icon: HardDrive, color: "#64748b", }, { value: "custom", label: "Custom S3", description: "Bring your own S3 endpoint for internal or niche providers.", icon: Cloud, color: "#14b8a6", }, ]; const PAGE_SIZE = 6; function createDraft(): DestinationDraft { return { name: "", provider: "awsS3", enabled: true, accessKeyId: "", secretAccessKey: "", hasSecretAccessKey: false, region: "ap-southeast-1", bucket: "", endpoint: "", additionalFlags: [], retentionDays: null, maxBackupCount: null, objectPrefix: "", serverId: null, }; } function getProviderMeta(provider: S3StorageProvider) { return ( PROVIDER_OPTIONS.find((option) => option.value === provider) || PROVIDER_OPTIONS[0] ); } function getDestinationSummary(destination: DestinationDraft) { const parts = [destination.bucket, destination.region] .map((value) => value.trim()) .filter(Boolean); if (destination.endpoint?.trim()) { parts.push(destination.endpoint.trim()); } return parts.length > 0 ? parts.join(" • ") : "Bucket and region are incomplete"; } function StatusButton({ active, loading, onClick, }: { active: boolean; loading?: boolean; onClick: () => void; }) { return ( ); } export default function S3StorageSettingsPanel({ destinations, servers, actionLoading, savingDestinationId, onUpsertDestination, onVerifyDestination, onToggleDestination, onDeleteDestination, }: S3StorageSettingsPanelProps) { const [draft, setDraft] = useState(null); const [editingId, setEditingId] = useState(null); const [currentPage, setCurrentPage] = useState(1); const totalItems = destinations.length; const totalPages = Math.max(1, Math.ceil(totalItems / PAGE_SIZE)); const safePage = Math.min(currentPage, totalPages); const startIndex = (safePage - 1) * PAGE_SIZE; const pagedDestinations = destinations.slice( startIndex, startIndex + PAGE_SIZE, ); const startItem = totalItems === 0 ? 0 : startIndex + 1; const endItem = totalItems === 0 ? 0 : startIndex + pagedDestinations.length; const openCreateModal = () => { setEditingId(null); setDraft(createDraft()); }; const openEditModal = (destination: S3StorageDestinationRecord) => { setEditingId(destination.id); setDraft({ id: destination.id, name: destination.name, provider: destination.provider, enabled: destination.enabled, accessKeyId: destination.accessKeyId, secretAccessKey: "", hasSecretAccessKey: destination.hasSecretAccessKey, region: destination.region, bucket: destination.bucket, endpoint: destination.endpoint, additionalFlags: [...destination.additionalFlags], retentionDays: destination.retentionDays, maxBackupCount: destination.maxBackupCount, objectPrefix: destination.objectPrefix, serverId: destination.serverId, }); }; const closeModal = () => { setEditingId(null); setDraft(null); }; const saveDraft = async () => { if (!draft) return; const success = await onUpsertDestination(draft); if (success) { closeModal(); } }; return ( <>
0 ? "1px solid var(--border)" : "none", }} >

Storage Destinations

Add and manage multiple S3-compatible storage destinations and target specific servers as needed.

{destinations.length === 0 ? (

No storage destinations yet

Create your first destination for global backup or specific target servers.

) : ( <>
{pagedDestinations.map((destination) => { const meta = getProviderMeta(destination.provider); const Icon = meta.icon; const isDeleting = savingDestinationId === `delete-${destination.id}`; const isVerifying = actionLoading === `verify-${destination.id}`; return ( ); })}
Destination Provider Target Server Connection Actions

{destination.name}

{destination.accessKeyId || "Access key not provided"}

{meta.label}

{meta.description}

{destination.targetServer?.name || "All accessible servers"}

{destination.targetServer?.ip || "Not restricted to specific servers"}

{getDestinationSummary(destination)}

{destination.additionalFlags.length > 0 ? `${destination.additionalFlags.length} additional flags` : "No additional flags"}

setCurrentPage(Math.min(Math.max(page, 1), totalPages)) } /> )}
{draft ? (

{editingId ? "Update Destination" : "Add Destination"}

Set the destination name, bucket, and target server that will use it.

Provider Type

{PROVIDER_OPTIONS.map((option) => { const Icon = option.icon; const selected = draft.provider === option.value; return ( ); })}

Lifecycle & Retention

Automatically remove old objects from this destination. Retention is disabled when both limits are empty.

Retention days setDraft((current) => current ? { ...current, retentionDays: event.target.value ? Number(event.target.value) : null, } : current, ) } placeholder="30" />
Maximum backup count setDraft((current) => current ? { ...current, maxBackupCount: event.target.value ? Number(event.target.value) : null, } : current, ) } placeholder="10" />
Object prefix / folder setDraft((current) => current ? { ...current, objectPrefix: event.target.value } : current, ) } placeholder="doktainer/backups/production" />

Required when retention or maximum count is enabled.

Basic Information

Set the destination name, bucket, and target server that will use it.

setDraft((current) => current ? { ...current, enabled: !current.enabled } : current, ) } />
Destination name setDraft((current) => current ? { ...current, name: event.target.value } : current, ) } placeholder="Example: Backup Singapore, R2 Europe, Archive Internal" />
Target server

Connection Settings

Access Key ID
setDraft((current) => current ? { ...current, accessKeyId: event.target.value, } : current, ) } placeholder="AKIA... or provider interoperability key" />
Secret Access Key {draft.hasSecretAccessKey ? " (leave blank to keep existing key)" : ""} setDraft((current) => current ? { ...current, secretAccessKey: event.target.value, hasSecretAccessKey: current.hasSecretAccessKey || Boolean(event.target.value.trim()), } : current, ) } />
Bucket setDraft((current) => current ? { ...current, bucket: event.target.value } : current, ) } placeholder="doktainer-backups" />
Region setDraft((current) => current ? { ...current, region: event.target.value } : current, ) } placeholder="ap-southeast-1" />
Endpoint {draft.provider === "awsS3" ? " (optional for AWS S3)" : " (required for S3-compatible providers)"} setDraft((current) => current ? { ...current, endpoint: event.target.value } : current, ) } placeholder="https://.r2.cloudflarestorage.com" />

Additional Flags

Save additional flags for runtime backup needs or provider compatibility.

{draft.additionalFlags.length === 0 ? (
No additional flags.
) : null} {draft.additionalFlags.map((flag, index) => (
setDraft((current) => { if (!current) return current; const nextFlags = [...current.additionalFlags]; nextFlags[index] = event.target.value; return { ...current, additionalFlags: nextFlags }; }) } placeholder="Contoh: forcePathStyle=true" />
))}

{getProviderMeta(draft.provider).label}

{getDestinationSummary(draft)}

) : null} ); }