refactor: page components and events shared module

- Home: usePerformanceData hook + performance UI components in _components
- License: extract enterprise section to components/license/enterprise-section
- Login: extract LoginForm to components/auth/login-form
- Events: lib/events (types, constants, getDisplayEvents) + components/events/columns
- Events page and buckets/events-tab use shared events module and getEventsColumns
This commit is contained in:
overtrue
2026-02-01 22:34:00 +08:00
parent a7391a10d3
commit e886f51e8f
15 changed files with 1527 additions and 1430 deletions
+73 -164
View File
@@ -2,48 +2,16 @@
import * as React from "react"
import { useTranslation } from "react-i18next"
import { RiAddLine, RiRefreshLine, RiDeleteBin7Line } from "@remixicon/react"
import { RiAddLine, RiRefreshLine } 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 { EventsNewForm } from "@/components/events/new-form"
import { getEventsColumns } from "@/components/events/columns"
import { useBucket } from "@/hooks/use-bucket"
import { useDialog } from "@/lib/feedback/dialog"
import { useMessage } from "@/lib/feedback/message"
import type { ColumnDef } from "@tanstack/react-table"
interface NotificationItem {
id: string
type: "Lambda" | "SQS" | "SNS" | "Topic"
arn: string
events: string[]
prefix?: string
suffix?: string
filterRules?: Array<{ Name: string; Value: string }>
}
const EVENT_DISPLAY_MAP: Record<string, string> = {
"s3:ObjectCreated:*": "PUT",
"s3:ObjectAccessed:*": "GET",
"s3:ObjectRemoved:*": "DELETE",
"s3:Replication:*": "REPLICA",
"s3:ObjectRestore:*": "RESTORE",
"s3:ObjectTransition:*": "RESTORE",
"s3:Scanner:ManyVersions": "SCANNER",
"s3:Scanner:BigPrefix": "SCANNER",
}
const TYPE_BADGE_CLASSES: Record<NotificationItem["type"], string> = {
Lambda: "bg-amber-100 text-amber-900 dark:bg-amber-900/40 dark:text-amber-100",
SQS: "bg-sky-100 text-sky-900 dark:bg-sky-900/40 dark:text-sky-100",
SNS: "bg-emerald-100 text-emerald-900 dark:bg-emerald-900/40 dark:text-emerald-100",
Topic: "bg-indigo-100 text-indigo-900 dark:bg-indigo-900/40 dark:text-indigo-100",
}
function getDisplayEvents(events: string[]) {
return [...new Set(events.map((e) => EVENT_DISPLAY_MAP[e] || e))]
}
import type { NotificationItem } from "@/lib/events"
interface BucketEventsTabProps {
bucketName: string
@@ -137,79 +105,76 @@ export function BucketEventsTab({ bucketName }: BucketEventsTabProps) {
loadData()
}, [loadData])
const columns: ColumnDef<NotificationItem>[] = React.useMemo(
() => [
{
id: "type",
header: () => t("Type"),
cell: ({ row }) => (
<Badge className={TYPE_BADGE_CLASSES[row.original.type] ?? ""}>
{row.original.type}
</Badge>
),
meta: { maxWidth: "7rem" },
},
{
id: "arn",
header: () => t("ARN"),
cell: ({ row }) => (
<span className="line-clamp-2 break-all font-medium">
{row.original.arn}
</span>
),
meta: { maxWidth: "180px" },
},
{
id: "events",
header: () => t("Events"),
cell: ({ row }) => (
<div className="flex flex-wrap gap-1">
{getDisplayEvents(row.original.events).map((event) => (
<Badge key={event} variant="secondary">
{event}
</Badge>
))}
</div>
),
meta: { maxWidth: "13rem" },
},
{
id: "prefix",
header: () => t("Prefix"),
cell: ({ row }) => <span>{row.original.prefix || "-"}</span>,
meta: { maxWidth: "9rem" },
},
{
id: "suffix",
header: () => t("Suffix"),
cell: ({ row }) => <span>{row.original.suffix || "-"}</span>,
meta: { maxWidth: "9rem" },
},
{
id: "actions",
header: () => t("Actions"),
enableSorting: false,
meta: { maxWidth: "6rem" },
cell: ({ row }) => (
<div className="flex justify-center">
<Button
type="button"
size="sm"
variant="outline"
className="gap-2"
onClick={(e) => {
e.stopPropagation()
handleRowDelete(row.original)
}}
>
<RiDeleteBin7Line className="size-4" aria-hidden />
<span>{t("Delete")}</span>
</Button>
</div>
),
},
],
[t]
const handleRowDelete = React.useCallback(
async (row: NotificationItem) => {
const confirmed = await new Promise<boolean>((resolve) => {
dialog.warning({
title: t("Confirm Delete"),
content: t(
"Are you sure you want to delete this notification configuration?"
),
positiveText: t("Delete"),
negativeText: t("Cancel"),
onPositiveClick: () => resolve(true),
onNegativeClick: () => resolve(false),
})
})
if (!confirmed) return
try {
setLoading(true)
const currentResponse = await listBucketNotifications(bucketName)
const currentNotifications = (currentResponse ?? {}) as unknown as Record<
string,
unknown[]
>
const configKey =
row.type === "Lambda"
? "LambdaFunctionConfigurations"
: row.type === "SQS"
? "QueueConfigurations"
: "TopicConfigurations"
const configs = (
currentNotifications as Record<string, Array<{ Id?: string }>>
)[configKey]
const updated = configs?.filter((c) => c.Id !== row.id) ?? []
const newConfig = {
...currentNotifications,
...(row.type === "Lambda"
? { LambdaFunctionConfigurations: updated }
: row.type === "SQS"
? { QueueConfigurations: updated }
: { TopicConfigurations: updated }),
}
await putBucketNotifications(bucketName, newConfig)
message.success(t("Delete Success"))
loadData()
} catch (error) {
message.error(
`${t("Delete Failed")}: ${(error as Error).message ?? error}`
)
} finally {
setLoading(false)
}
},
[
bucketName,
dialog,
listBucketNotifications,
loadData,
message,
putBucketNotifications,
t,
]
)
const columns = React.useMemo(
() => getEventsColumns(t, handleRowDelete),
[t, handleRowDelete]
)
const { table } = useDataTable<NotificationItem>({
@@ -218,62 +183,6 @@ export function BucketEventsTab({ bucketName }: BucketEventsTabProps) {
getRowId: (row) => row.id,
})
const handleRowDelete = async (row: NotificationItem) => {
const confirmed = await new Promise<boolean>((resolve) => {
dialog.warning({
title: t("Confirm Delete"),
content: t(
"Are you sure you want to delete this notification configuration?"
),
positiveText: t("Delete"),
negativeText: t("Cancel"),
onPositiveClick: () => resolve(true),
onNegativeClick: () => resolve(false),
})
})
if (!confirmed) return
try {
setLoading(true)
const currentResponse = await listBucketNotifications(bucketName)
const currentNotifications = (currentResponse ?? {}) as unknown as Record<
string,
unknown[]
>
const configKey =
row.type === "Lambda"
? "LambdaFunctionConfigurations"
: row.type === "SQS"
? "QueueConfigurations"
: "TopicConfigurations"
const configs = (
currentNotifications as Record<string, Array<{ Id?: string }>>
)[configKey]
const updated = configs?.filter((c) => c.Id !== row.id) ?? []
const newConfig = {
...currentNotifications,
...(row.type === "Lambda"
? { LambdaFunctionConfigurations: updated }
: row.type === "SQS"
? { QueueConfigurations: updated }
: { TopicConfigurations: updated }),
}
await putBucketNotifications(bucketName, newConfig)
message.success(t("Delete Success"))
loadData()
} catch (error) {
message.error(
`${t("Delete Failed")}: ${(error as Error).message ?? error}`
)
} finally {
setLoading(false)
}
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">