Files
rustfs-console/components/buckets/replication-tab.tsx
T

210 lines
6.5 KiB
TypeScript

"use client"
import * as React from "react"
import { useTranslation } from "react-i18next"
import { RiAddLine, RiRefreshLine, RiDeleteBin7Line } from "@remixicon/react"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { DataTable } from "@/components/data-table/data-table"
import { useDataTable } from "@/hooks/use-data-table"
import { useBucket } from "@/hooks/use-bucket"
import { usePermissions } from "@/hooks/use-permissions"
import { ReplicationNewForm } from "@/components/replication/new-form"
import { useDialog } from "@/lib/feedback/dialog"
import { useMessage } from "@/lib/feedback/message"
import type { ColumnDef } from "@tanstack/react-table"
interface ReplicationRule {
ID?: string
Status?: string
Priority?: number
Filter?: { Prefix?: string }
Destination?: { Bucket?: string; StorageClass?: string }
}
interface BucketReplicationTabProps {
bucketName: string
}
export function BucketReplicationTab({ bucketName }: BucketReplicationTabProps) {
const { t } = useTranslation()
const message = useMessage()
const dialog = useDialog()
const { canCapability } = usePermissions()
const { getBucketReplication, putBucketReplication, deleteBucketReplication, deleteRemoteReplicationTarget } =
useBucket()
const replicationContext = React.useMemo(() => ({ bucket: bucketName }), [bucketName])
const canEditReplication = canCapability("bucket.replication.edit", replicationContext)
const [data, setData] = React.useState<ReplicationRule[]>([])
const [loading, setLoading] = React.useState(false)
const [newFormOpen, setNewFormOpen] = React.useState(false)
const loadData = React.useCallback(async () => {
setLoading(true)
try {
const res = await getBucketReplication(bucketName)
setData(res?.ReplicationConfiguration?.Rules ?? [])
} catch {
setData([])
} finally {
setLoading(false)
}
}, [bucketName, getBucketReplication])
React.useEffect(() => {
loadData()
}, [loadData])
const handleRowDelete = React.useCallback(
async (rule: ReplicationRule) => {
if (!canEditReplication) return
const remaining = data.filter((item) => item !== rule)
try {
if (remaining.length === 0) {
await deleteBucketReplication(bucketName)
await deleteRemoteReplicationTarget(bucketName, rule.Destination?.Bucket ?? "")
} else {
const currentConfig = await getBucketReplication(bucketName)
const role = currentConfig?.ReplicationConfiguration?.Role
if (!role) {
throw new Error("Replication configuration Role is missing")
}
await putBucketReplication(bucketName, {
Role: role,
Rules: remaining,
})
}
message.success(t("Delete Success"))
loadData()
} catch (error) {
message.error((error as Error).message || t("Delete Failed"))
}
},
[
canEditReplication,
data,
deleteBucketReplication,
bucketName,
deleteRemoteReplicationTarget,
getBucketReplication,
putBucketReplication,
message,
t,
loadData,
],
)
const confirmDelete = React.useCallback(
(rule: ReplicationRule) => {
dialog.error({
title: t("Warning"),
content: t("Are you sure you want to delete this replication rule?"),
positiveText: t("Confirm"),
negativeText: t("Cancel"),
onPositiveClick: () => handleRowDelete(rule),
})
},
[dialog, t, handleRowDelete],
)
const columns: ColumnDef<ReplicationRule>[] = React.useMemo(
() => [
{
accessorKey: "ID",
header: () => t("Rule ID"),
cell: ({ row }) => <span>{row.original.ID || "-"}</span>,
},
{
accessorKey: "Status",
header: () => t("Status"),
cell: ({ row }) => (
<Badge variant={row.original.Status === "Enabled" ? "secondary" : "outline"}>
{row.original.Status === "Enabled" ? t("Enabled") : t("Disabled")}
</Badge>
),
},
{
accessorKey: "Priority",
header: () => t("Priority"),
cell: ({ row }) => <span>{String(row.original.Priority ?? "-")}</span>,
},
{
id: "Filter",
header: () => t("Prefix"),
cell: ({ row }) => <span>{row.original.Filter?.Prefix || "-"}</span>,
},
{
id: "destination-bucket",
header: () => t("Destination Bucket"),
cell: ({ row }) => {
const bucketArn = row.original.Destination?.Bucket || ""
return <span>{bucketArn.replace(/^arn:aws:s3:::/, "") || "-"}</span>
},
},
{
id: "destination-storage",
header: () => t("Storage Class"),
cell: ({ row }) => <span>{row.original.Destination?.StorageClass || "-"}</span>,
},
{
id: "actions",
header: () => t("Actions"),
enableSorting: false,
cell: ({ row }) => (
<div className="flex items-center gap-2">
{canEditReplication ? (
<Button variant="outline" size="sm" onClick={() => confirmDelete(row.original)}>
<RiDeleteBin7Line className="size-4" aria-hidden />
<span>{t("Delete")}</span>
</Button>
) : null}
</div>
),
},
],
[canEditReplication, t, confirmDelete],
)
const { table } = useDataTable<ReplicationRule>({
data,
columns,
getRowId: (row) => row.ID ?? JSON.stringify(row),
})
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">{t("Bucket Replication")}</h2>
<div className="flex gap-2">
{canEditReplication ? (
<Button variant="outline" onClick={() => setNewFormOpen(true)}>
<RiAddLine className="size-4" />
<span>{t("Add Replication Rule")}</span>
</Button>
) : null}
<Button variant="outline" onClick={loadData}>
<RiRefreshLine className="size-4" />
<span>{t("Refresh")}</span>
</Button>
</div>
</div>
<DataTable
table={table}
isLoading={loading}
emptyTitle={t("No Data")}
emptyDescription={t("Add replication rules to sync objects across buckets.")}
/>
<ReplicationNewForm
open={newFormOpen}
onOpenChange={setNewFormOpen}
bucketName={bucketName}
onSuccess={loadData}
/>
</div>
)
}