diff --git a/app/(auth)/auth/login/page.tsx b/app/(auth)/auth/login/page.tsx index 3580948..891b4cf 100644 --- a/app/(auth)/auth/login/page.tsx +++ b/app/(auth)/auth/login/page.tsx @@ -1,31 +1,14 @@ "use client" -import { useEffect, Suspense } from "react" -import Link from "next/link" -import Image from "next/image" +import { useEffect, Suspense, useState } from "react" import { useSearchParams } from "next/navigation" import { useRouter } from "next/navigation" import { useTranslation } from "react-i18next" -import { RiSettings3Line } from "@remixicon/react" -import { Input } from "@/components/ui/input" -import { Button } from "@/components/ui/button" -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { - Field, - FieldContent, - FieldLabel, -} from "@/components/ui/field" -import { ThemeSwitcher } from "@/components/theme-switcher" -import { LanguageSwitcher } from "@/components/language-switcher" -import { AuthHeroStatic } from "@/components/auth/heroes/hero-static" +import { LoginForm, type LoginMethod } from "@/components/auth/login-form" import { useAuth } from "@/contexts/auth-context" import { useMessage } from "@/lib/feedback/message" import { buildRoute } from "@/lib/routes" -import logoImage from "@/assets/logo.svg" import { configManager } from "@/lib/config" -import { useState } from "react" - -type LoginMethod = "accessKeyAndSecretKey" | "sts" export default function LoginPage() { return ( @@ -70,9 +53,7 @@ function LoginPageContent() { e.preventDefault() const credentials = - method === "accessKeyAndSecretKey" - ? accessKeyAndSecretKey - : sts + method === "accessKeyAndSecretKey" ? accessKeyAndSecretKey : sts try { const currentConfig = await configManager.loadConfig() @@ -97,227 +78,3 @@ function LoginPageContent() { /> ) } - -function LoginForm({ - method, - setMethod, - accessKeyAndSecretKey, - setAccessKeyAndSecretKey, - sts, - setSts, - handleLogin, -}: { - method: LoginMethod - setMethod: (m: LoginMethod) => void - accessKeyAndSecretKey: { accessKeyId: string; secretAccessKey: string } - setAccessKeyAndSecretKey: React.Dispatch< - React.SetStateAction<{ accessKeyId: string; secretAccessKey: string }> - > - sts: { accessKeyId: string; secretAccessKey: string; sessionToken: string } - setSts: React.Dispatch< - React.SetStateAction<{ - accessKeyId: string - secretAccessKey: string - sessionToken: string - }> - > - handleLogin: (e: React.FormEvent) => void -}) { - const { t } = useTranslation() - - return ( -
- -
-
- -
-
-
- - - -
- -
- RustFS - -
- setMethod(v as LoginMethod)} - className="flex flex-col gap-4" - > - - - {t("Key Login")} - - - {t("STS Login")} - - - -
-
- {method === "accessKeyAndSecretKey" ? ( - <> - - - {t("Account")} - - - - setAccessKeyAndSecretKey((prev) => ({ - ...prev, - accessKeyId: e.target.value, - })) - } - autoComplete="username" - type="text" - placeholder={t("Please enter account")} - /> - - - - {t("Key")} - - - setAccessKeyAndSecretKey((prev) => ({ - ...prev, - secretAccessKey: e.target.value, - })) - } - autoComplete="current-password" - type="password" - placeholder={t("Please enter key")} - /> - - - - ) : ( - <> - - - {t("STS Username")} - - - - setSts((prev) => ({ - ...prev, - accessKeyId: e.target.value, - })) - } - autoComplete="new-password" - type="text" - placeholder={t("Please enter STS username")} - /> - - - - - {t("STS Key")} - - - - setSts((prev) => ({ - ...prev, - secretAccessKey: e.target.value, - })) - } - autoComplete="new-password" - type="password" - placeholder={t("Please enter STS key")} - /> - - - - - {t("STS Session Token")} - - - - setSts((prev) => ({ - ...prev, - sessionToken: e.target.value, - })) - } - autoComplete="new-password" - type="text" - placeholder={t( - "Please enter STS session token" - )} - /> - - - - )} - - -
-
-
-
- -
-

- {t("Login Problems?")}{" "} - - {t("Get Help")} - -

-
- -
-
- -
-
- -
-
-
-
-
-
- ) -} - diff --git a/app/(dashboard)/_components/performance-backend-card.tsx b/app/(dashboard)/_components/performance-backend-card.tsx new file mode 100644 index 0000000..ec46f43 --- /dev/null +++ b/app/(dashboard)/_components/performance-backend-card.tsx @@ -0,0 +1,63 @@ +"use client" + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" + +export interface BackendInfoItem { + icon: React.ComponentType<{ className?: string; "aria-hidden"?: boolean | "true" | "false" }> + title: string + value?: string +} + +export function PerformanceBackendCard({ + items, + t, +}: { + items: BackendInfoItem[] + t: (key: string) => string +}) { + return ( + + + {items.length ? t("Backend Services") : ""} + + {items.length + ? t( + "Key services and configuration values reported by the cluster." + ) + : ""} + + + +
+ {items.map((item) => ( + + + + {item.title} + + + + +

+ {item.value ?? "-"} +

+
+
+ ))} +
+
+
+ ) +} diff --git a/app/(dashboard)/_components/performance-infrastructure-card.tsx b/app/(dashboard)/_components/performance-infrastructure-card.tsx new file mode 100644 index 0000000..e0188a1 --- /dev/null +++ b/app/(dashboard)/_components/performance-infrastructure-card.tsx @@ -0,0 +1,86 @@ +"use client" + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" + +export function PerformanceInfrastructureCard({ + onlineServers, + offlineServers, + onlineDisks, + offlineDisks, + t, +}: { + onlineServers: number + offlineServers: number + onlineDisks: number + offlineDisks: number + t: (key: string) => string +}) { + return ( + + + {t("Infrastructure Health")} + + {t( + "Real-time status of cluster servers and backend storage devices." + )} + + + +
+
+

+ {t("Servers")} +

+
+
+

+ {t("Online")} +

+

+ {onlineServers} +

+
+
+

+ {t("Offline")} +

+

+ {offlineServers} +

+
+
+
+
+

+ {t("Disks")} +

+
+
+

+ {t("Online")} +

+

+ {onlineDisks} +

+
+
+

+ {t("Offline")} +

+

+ {offlineDisks} +

+
+
+
+
+
+
+ ) +} diff --git a/app/(dashboard)/_components/performance-server-list.tsx b/app/(dashboard)/_components/performance-server-list.tsx new file mode 100644 index 0000000..0240ed7 --- /dev/null +++ b/app/(dashboard)/_components/performance-server-list.tsx @@ -0,0 +1,158 @@ +"use client" + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Progress } from "@/components/ui/progress" +import { ScrollArea } from "@/components/ui/scroll-area" +import { niceBytes } from "@/lib/functions" +import { cn } from "@/lib/utils" +import dayjs from "dayjs" +import relativeTime from "dayjs/plugin/relativeTime" +import type { ServerInfo } from "@/hooks/use-performance-data" + +dayjs.extend(relativeTime) + +function countOnlineDrives(server: ServerInfo, type: string) { + return (server?.drives || []).filter((d) => d.state === type).length +} + +function countOnlineNetworks(server: ServerInfo, type: string) { + return Object.values(server?.network || {}).filter( + (state) => state === type + ).length +} + +export function PerformanceServerList({ + servers, + t, +}: { + servers: ServerInfo[] + t: (key: string) => string +}) { + return ( + + +
+ {t("Server List")} + + {t( + "Inspect individual server health, disk utilization, and network status." + )} + +
+ + {t("Total")}: {servers.length} + +
+ + + {servers.map((server, index) => ( + + +
+
+ + + {server.endpoint ?? "--"} + +
+
+ + {t("Disks")}:{" "} + {countOnlineDrives(server, "ok")} /{" "} + {server.drives?.length ?? 0} + + + {t("Network")}:{" "} + {countOnlineNetworks(server, "online")} /{" "} + {Object.keys(server.network ?? {}).length} + + + {t("Uptime")}:{" "} + {server.uptime != null + ? dayjs() + .subtract(server.uptime, "second") + .fromNow() + : "--"} + +
+
+
+ +

+ {t("Version")}: {server.version ?? "--"} +

+ +
+ {(server.drives || []).map((drive) => ( + + + + {drive.drive_path ?? "--"} + + + {niceBytes(String(drive.usedspace ?? 0))} /{" "} + {niceBytes(String(drive.totalspace ?? 0))} + + + + +
+

+ {t("Used")}:{" "} + + {niceBytes(String(drive.usedspace ?? 0))} + +

+

+ {t("Available")}:{" "} + + {niceBytes(String(drive.availspace ?? 0))} + +

+
+
+
+ ))} +
+
+
+
+ ))} +
+
+
+ ) +} diff --git a/app/(dashboard)/_components/performance-summary-cards.tsx b/app/(dashboard)/_components/performance-summary-cards.tsx new file mode 100644 index 0000000..b571e13 --- /dev/null +++ b/app/(dashboard)/_components/performance-summary-cards.tsx @@ -0,0 +1,70 @@ +"use client" + +import * as React from "react" +import Link from "next/link" +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { cn } from "@/lib/utils" + +export interface SummaryMetric { + label: string + display: string + icon: React.ComponentType<{ className?: string; "aria-hidden"?: boolean | "true" | "false" }> + caption: string | null + href?: string +} + +export function PerformanceSummaryCards({ + metrics, +}: { + metrics: SummaryMetric[] +}) { + return ( +
+ {metrics.map((metric) => { + const cardContent = ( + + + + {metric.label} + + + + +
+

+ {metric.display} +

+ {metric.caption ? ( +

+ {metric.caption} +

+ ) : null} +
+
+
+ ) + return metric.href ? ( + + {cardContent} + + ) : ( + {cardContent} + ) + })} +
+ ) +} diff --git a/app/(dashboard)/_components/performance-usage-card.tsx b/app/(dashboard)/_components/performance-usage-card.tsx new file mode 100644 index 0000000..8e4c6f7 --- /dev/null +++ b/app/(dashboard)/_components/performance-usage-card.tsx @@ -0,0 +1,89 @@ +"use client" + +import { RiDatabase2Line } from "@remixicon/react" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Progress } from "@/components/ui/progress" +import { niceBytes } from "@/lib/functions" + +export interface UsageStat { + label: string + value: string +} + +export function PerformanceUsageCard({ + lastUpdatedLabel, + totalUsedCapacity, + usedPercent, + usageStats, + t, +}: { + lastUpdatedLabel: string + totalUsedCapacity: number + usedPercent: number + usageStats: UsageStat[] + t: (key: string) => string +}) { + return ( + + +
+ {t("Usage Report")} + + {t("Last Scan Activity")}: {lastUpdatedLabel} + +
+ + {t( + "Monitor overall storage usage and recent scanner activity at a glance." + )} + +
+ +
+
+ +
+

+ {t("Used Capacity")} +

+

+ {niceBytes(String(totalUsedCapacity))} +

+
+
+
+ +

+ {usedPercent.toFixed(0)}% +

+
+
+ +
+ {usageStats.map((item) => ( +
+

+ {item.label} +

+

+ {item.value} +

+
+ ))} +
+
+
+ ) +} diff --git a/app/(dashboard)/events/page.tsx b/app/(dashboard)/events/page.tsx index 4e0b2ce..0375d5d 100644 --- a/app/(dashboard)/events/page.tsx +++ b/app/(dashboard)/events/page.tsx @@ -1,57 +1,20 @@ "use client" -import * as React from "react" import { useState, useEffect, useCallback, useMemo } 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 { Page } from "@/components/page" import { PageHeader } from "@/components/page-header" import { BucketSelector } from "@/components/buckets/selector" 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 = { - "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 = { - 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" export default function EventsPage() { const { t } = useTranslation() @@ -141,88 +104,77 @@ export default function EventsPage() { loadData() }, [loadData]) - const columns: ColumnDef[] = useMemo( - () => [ - { - id: "type", - header: () => t("Type"), - cell: ({ row }) => ( - - {row.original.type} - - ), - meta: { maxWidth: "7rem" }, - }, - { - id: "arn", - header: () => t("ARN"), - cell: ({ row }) => ( - - {row.original.arn} - - ), - meta: { maxWidth: "180px" }, - }, - { - id: "events", - header: () => t("Events"), - cell: ({ row }) => ( -
- {getDisplayEvents(row.original.events).map((event) => ( - - {event} - - ))} -
- ), - meta: { maxWidth: "13rem" }, - }, - { - id: "prefix", - header: () => t("Prefix"), - cell: ({ row }) => ( - {row.original.prefix || "-"} - ), - meta: { maxWidth: "9rem" }, - }, - { - id: "suffix", - header: () => t("Suffix"), - cell: ({ row }) => ( - {row.original.suffix || "-"} - ), - meta: { maxWidth: "9rem" }, - }, - { - id: "actions", - header: () => t("Actions"), - enableSorting: false, - meta: { maxWidth: "6rem" }, - cell: ({ row }) => ( -
- -
- ), - }, - ], - // eslint-disable-next-line react-hooks/exhaustive-deps -- handleRowDelete used in cell, stable ref - [t] + const handleRowDelete = useCallback( + async (row: NotificationItem) => { + const confirmed = await new Promise((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 || !bucketName) 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> + )[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) { + console.error(t("Delete Failed"), error) + message.error( + `${t("Delete Failed")}: ${(error as Error).message ?? error}` + ) + } finally { + setLoading(false) + } + }, + [ + bucketName, + dialog, + listBucketNotifications, + loadData, + message, + putBucketNotifications, + t, + ] + ) + + const columns = useMemo( + () => getEventsColumns(t, handleRowDelete), + [t, handleRowDelete] ) const { table } = useDataTable({ @@ -231,63 +183,6 @@ export default function EventsPage() { getRowId: (row) => row.id, }) - const handleRowDelete = async (row: NotificationItem) => { - const confirmed = await new Promise((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 || !bucketName) 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>)[ - 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) { - console.error(t("Delete Failed"), error) - message.error( - `${t("Delete Failed")}: ${(error as Error).message || error}` - ) - } finally { - setLoading(false) - } - } - return ( [ - { label: t("Licensed Company"), value: t("No License") }, - { label: t("License Key"), value: licenseKey }, - { label: t("Licensed Users"), value: t("Unlimited") }, - { - label: t("Support Level"), - value: `${t("Enterprise")} (7x24x365)`, - }, - ], - [t] - ) - - const serviceInfo = useMemo( - () => [ - { label: t("Service Hotline"), value: "400-033-5363" }, - { label: t("Version"), value: "v2.3" }, - { label: t("Service Email"), value: "hello@rustfs.com" }, - { - label: t("Enterprise Service Level"), - value: t("Platinum Service"), - }, - { - label: t("On-site Technical Service"), - value: t("Supported"), - }, - { - label: t("Remote Technical Support"), - value: t("Supported"), - }, - { label: t("Technical Training"), value: t("Supported") }, - { label: t("On-site Deployment"), value: t("Supported") }, - { label: t("Emergency Response"), value: t("Supported") }, - { - label: t("Response Level"), - value: t("One-hour Response"), - }, - ], - [t] - ) - - const permissions = useMemo( - () => [ - { - name: t("Single Machine Multiple Disks"), - description: t( - "Supports managing multiple storage disks on a single server to improve storage resource utilization and simplify management and maintenance" - ), - status: t("Enabled"), - }, - { - name: t("Advanced Monitoring"), - description: t( - "Provides detailed performance monitoring and alerting mechanisms to help administrators understand system status in real-time and ensure system stability and reliability" - ), - status: t("Enabled"), - }, - { - name: t("Metrics"), - description: t( - "Collects and displays key performance indicators (such as CPU, memory, disk I/O, etc.) through visualized charts to help users understand system operation status and performance bottlenecks" - ), - status: t("Enabled"), - }, - ], - [t] - ) - - const permissionsColumns: ColumnDef[] = useMemo( - () => [ - { - id: "name", - header: () => t("Name"), - cell: ({ row }) => ( - {row.original.name} - ), - }, - { - id: "description", - header: () => t("Description"), - cell: ({ row }) => ( - - {row.original.description} - - ), - }, - { - id: "status", - header: () => t("Status"), - cell: ({ row }) => ( - {row.original.status} - ), - }, - ], - [t] - ) - - const { table: permissionsTable } = useDataTable({ - data: permissions, - columns: permissionsColumns, - getRowId: (row) => row.name, - }) - - const technicalParameters = useMemo( - () => [ - { label: t("Service Hotline"), value: "400-033-5363" }, - { - label: t("Supported OS"), - value: "Windows、Linux、MacOS", - }, - { - label: t("Supported CPU Architecture"), - value: "Amd64、ARM、AMR64、MIPS64、S390X、PPC64LE", - }, - { - label: t("Virtualization Platform Support"), - value: "KVM、VMware、Hyper-V、Docker、Kubernetes", - }, - { - label: t("Development Language Requirements"), - value: "C++、Java、Rust、Go、Python、Node.js", - }, - { label: t("SNND Mode"), value: t("Supported") }, - { label: t("SNMD Mode"), value: t("Supported") }, - { label: t("MNMD Mode"), value: t("Supported") }, - { label: t("Bucket Count"), value: t("Unlimited") }, - { label: t("Object Count"), value: t("Unlimited") }, - { - label: t("EC Mode"), - value: t("Reed-Solomon Matrix"), - }, - { label: t("Access Control"), value: "IAM Policy" }, - { - label: t("Secure Transport"), - value: t("Supports HTTPS, TLS"), - }, - { - label: t("Bucket Policy"), - value: t("Public, Private, Custom"), - }, - { label: t("Single Object"), value: t("Max 50TB") }, - { - label: t("Data Redundancy"), - value: t("Supports Erasure Coding"), - }, - { label: t("Data Backup"), value: t("Supported") }, - { label: t("Scalability"), value: t("Supported") }, - { - label: t("Read/Write Performance"), - value: t("Supports high concurrency operations"), - }, - { - label: t("Identity Authentication Expansion"), - value: "OpenID、LDAP", - }, - { label: t("S3 Compatibility"), value: t("Supported") }, - { - label: t("SDK Support"), - value: "Java、Python、Go、Rust、Node.js", - }, - { label: t("Bucket Notification"), value: t("Supported") }, - { label: t("RustyVault Encryption"), value: t("Supported") }, - { label: t("HashiCorp Encryption"), value: t("Supported") }, - { label: t("Lifecycle Management"), value: t("Supported") }, - { label: t("s3fs"), value: t("Supported") }, - { label: t("Prometheus"), value: t("Supported") }, - { label: t("Bucket Quota"), value: t("Supported") }, - { label: t("Audit"), value: t("Supported") }, - { label: t("Logs"), value: t("Supported") }, - { label: t("Object Repair"), value: t("Supported") }, - { label: t("WORM"), value: t("Supported") }, - { label: t("Remote Tiering"), value: t("Supported") }, - { label: t("Tiering Transfer"), value: t("Supported") }, - { label: t("Object Sharing"), value: t("Supported") }, - { label: t("Load Balancing"), value: t("Supported") }, - { label: t("Object Tags"), value: t("Supported") }, - { label: t("Multipart Upload"), value: t("Supported") }, - { label: t("Key Creation"), value: t("Supported") }, - { label: t("Key Expiration"), value: t("Supported") }, - { - label: t("Disk Bad Spot Check"), - value: t("Supported"), - }, - { label: t("Bitrot"), value: t("Supported") }, - { label: t("Version Control"), value: t("Supported") }, - ], - [t] - ) - - const contactSupport = () => { - window.open( - "https://ww18.53kf.com/webCompany.php?arg=11003151&kf_sign=DA4MDMTc0Ng4MjE1MjEzODAyNDkyMDAyNzMwMDMxNTE%253D&style=2", - "_blank" - ) - } - - const updateLicense = () => { - // Placeholder logic - } - - return ( - <> - -

{t("Enterprise License")}

-
- -
- - -
-
-
- - {t("Enterprise License")} - - - {t("Status")}: - {hasValidLicense ? t("Normal") : t("Expired")} - -
-

- {t("License Valid Until")}:{endDate} -

-
- -
- - -
-
-
-
- -
- - -

- {t("License Details")} -

-
- {licenseDetails.map((item) => ( -
-
- {item.label} -
-
- {item.value} -
-
- ))} -
-
-
- - - -

- {t("Customer Service")} -

-
- {serviceInfo.map((item) => ( -
-
- {item.label} -
-
- {item.value} -
-
- ))} -
-
-
-
- - - -

- {t("Feature Permissions")} -

- -
-
- - - -

- {t("Technical Parameters")} -

-
- {technicalParameters.map((item) => ( -
-
- {item.label} -
-
- {item.value} -
-
- ))} -
-
-
-
- - ) -} export default function LicensePage() { if (hasLicense) { return ( - + ) } diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index a751a35..dc83e27 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -1,110 +1,43 @@ "use client" import * as React from "react" -import Link from "next/link" import { Page } from "@/components/page" import { PageHeader } from "@/components/page-header" -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion" import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Progress } from "@/components/ui/progress" -import { ScrollArea } from "@/components/ui/scroll-area" import { Spinner } from "@/components/ui/spinner" -import { useSystem } from "@/hooks/use-system" +import { usePerformanceData } from "@/hooks/use-performance-data" import { niceBytes } from "@/lib/functions" -import { cn } from "@/lib/utils" -import { - RiArchiveDrawerFill, - RiArchiveLine, - RiDatabase2Line, - RiHardDrive2Line, - RiListSettingsFill, - RiRefreshLine, - RiSecurePaymentFill, - RiStackLine, -} from "@remixicon/react" +import { RiArchiveDrawerFill, RiArchiveLine, RiHardDrive2Line, RiListSettingsFill, RiRefreshLine, RiSecurePaymentFill, RiStackLine } from "@remixicon/react" import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useMemo } from "react" import { useTranslation } from "react-i18next" +import { PerformanceSummaryCards } from "./_components/performance-summary-cards" +import { PerformanceUsageCard } from "./_components/performance-usage-card" +import { PerformanceInfrastructureCard } from "./_components/performance-infrastructure-card" +import { PerformanceBackendCard } from "./_components/performance-backend-card" +import { PerformanceServerList } from "./_components/performance-server-list" dayjs.extend(relativeTime) -interface ServerInfo { - endpoint?: string - state?: string - version?: string - uptime?: number - drives?: Array<{ - uuid?: string - drive_path?: string - usedspace?: number - totalspace?: number - availspace?: number - state?: string - }> - network?: Record -} - -interface SystemInfo { - buckets?: { count?: number } - objects?: { count?: number } - servers?: ServerInfo[] - backend?: { - backendType?: string - onlineDisks?: number - offlineDisks?: number - } -} - -interface DataUsageInfo { - total_capacity?: number - total_used_capacity?: number -} - -interface StorageInfo { - backend?: { - StandardSCParity?: string - RRSCParity?: string - } -} - -interface MetricsInfo { - aggregated?: { - scanner?: { - current_started?: string - cycle_complete_times?: string[] - } - } -} - export default function PerformancePage() { const { t } = useTranslation() - const systemApi = useSystem() - - const [metricsInfo, setMetricsInfo] = useState({}) - const [systemInfo, setSystemInfo] = useState({}) - const [datausageinfo, setDatausageinfo] = useState({}) - const [storageinfo, setStorageinfo] = useState({}) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const mountedRef = React.useRef(true) + const { + systemInfo, + metricsInfo, + datausageinfo, + storageinfo, + loading, + error, + refetch, + } = usePerformanceData() const numberFormatter = useMemo(() => new Intl.NumberFormat(), []) const storageBackend = useMemo( - () => storageinfo?.backend ?? (storageinfo as { Backend?: StorageInfo["backend"] })?.Backend, + () => + storageinfo?.backend ?? + (storageinfo as { Backend?: typeof storageinfo.backend })?.Backend, [storageinfo] ) @@ -161,9 +94,7 @@ export default function PerformancePage() { }, [metricsInfo]) const fromLastScanTime = useMemo(() => { - const start = dayjs( - metricsInfo?.aggregated?.scanner?.current_started - ) + const start = dayjs(metricsInfo?.aggregated?.scanner?.current_started) if (!start.isValid()) return "--" return dayjs().from(start) }, [metricsInfo]) @@ -183,35 +114,21 @@ export default function PerformancePage() { const usageStats = useMemo( () => [ - { - label: t("Last Normal Operation"), - value: fromLastStartTime, - }, - { - label: t("Last Scan Activity"), - value: fromLastScanTime, - }, - { - label: t("Uptime"), - value: lastScanTime, - }, + { label: t("Last Normal Operation"), value: fromLastStartTime }, + { label: t("Last Scan Activity"), value: fromLastScanTime }, + { label: t("Uptime"), value: lastScanTime }, ], [t, fromLastStartTime, fromLastScanTime, lastScanTime] ) const onlineServers = useMemo( - () => - (systemInfo?.servers || []).filter( - (s) => s.state === "online" - ).length, + () => (systemInfo?.servers || []).filter((s) => s.state === "online").length, [systemInfo] ) const offlineServers = useMemo( () => - (systemInfo?.servers || []).filter( - (s) => s.state === "offline" - ).length, + (systemInfo?.servers || []).filter((s) => s.state === "offline").length, [systemInfo] ) @@ -236,62 +153,15 @@ export default function PerformancePage() { [systemInfo, storageBackend, t] ) - const countOnlineDrives = (server: ServerInfo, type: string) => - (server?.drives || []).filter((d) => d.state === type).length - - const countOnlineNetworks = ( - server: ServerInfo, - type: string - ) => - Object.values(server?.network || {}).filter( - (state) => state === type - ).length - - const getPageData = useCallback(async () => { - if (!mountedRef.current) return - setLoading(true) - setError(null) - try { - const [sysRes, usageRes, storageRes] = await Promise.all([ - systemApi.getSystemInfo(), - systemApi.getDataUsageInfo(), - systemApi.getStorageInfo(), - ]) - if (!mountedRef.current) return - setSystemInfo((sysRes as SystemInfo) ?? {}) - setDatausageinfo((usageRes as DataUsageInfo) ?? {}) - setStorageinfo((storageRes as StorageInfo) ?? {}) - } catch (err) { - if (!mountedRef.current) return - console.error("Failed to load performance data:", err) - setError((err as Error)?.message ?? t("Get Data Failed")) - } finally { - if (mountedRef.current) setLoading(false) - } - try { - const metricsRes = await systemApi.getSystemMetrics() - if (mountedRef.current) setMetricsInfo((metricsRes as MetricsInfo) ?? {}) - } catch { - if (mountedRef.current) setMetricsInfo({}) - } - }, [systemApi, t]) - - useEffect(() => { - mountedRef.current = true - void getPageData() - return () => { - mountedRef.current = false - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- fetch once on mount; systemApi methods are stable - }, []) - - if (loading && !Object.keys(systemInfo).length && !Object.keys(datausageinfo).length) { + if ( + loading && + !Object.keys(systemInfo).length && + !Object.keys(datausageinfo).length + ) { return ( -

- {t("Server Information")} -

+

{t("Server Information")}

@@ -305,15 +175,13 @@ export default function PerformancePage() { + } > -

- {t("Server Information")} -

+

{t("Server Information")}

{error}

@@ -326,334 +194,40 @@ export default function PerformancePage() { + } > -

- {t("Server Information")} -

+

{t("Server Information")}

-
- {summaryMetrics.map((metric) => { - const cardContent = ( - - - - {metric.label} - - - - -
-

- {metric.display} -

- {metric.caption ? ( -

- {metric.caption} -

- ) : null} -
-
-
- ) - return metric.href ? ( - - {cardContent} - - ) : ( - {cardContent} - ) - })} -
+ - - -
- {t("Usage Report")} - - {t("Last Scan Activity")}: {lastUpdatedLabel} - -
- - {t( - "Monitor overall storage usage and recent scanner activity at a glance." - )} - -
- -
-
- -
-

- {t("Used Capacity")} -

-

- {niceBytes( - String(datausageinfo.total_used_capacity ?? 0) - )} -

-
-
-
- -

- {usedPercent.toFixed(0)}% -

-
-
+ -
- {usageStats.map((item) => ( -
-

- {item.label} -

-

- {item.value} -

-
- ))} -
-
-
+ - - - {t("Infrastructure Health")} - - {t( - "Real-time status of cluster servers and backend storage devices." - )} - - - -
-
-

- {t("Servers")} -

-
-
-

- {t("Online")} -

-

- {onlineServers} -

-
-
-

- {t("Offline")} -

-

- {offlineServers} -

-
-
-
-
-

- {t("Disks")} -

-
-
-

- {t("Online")} -

-

- {systemInfo?.backend?.onlineDisks ?? 0} -

-
-
-

- {t("Offline")} -

-

- {systemInfo?.backend?.offlineDisks ?? 0} -

-
-
-
-
-
-
+ - - - {t("Backend Services")} - - {t( - "Key services and configuration values reported by the cluster." - )} - - - -
- {backendInfo.map((item) => ( - - - - {item.title} - - - - -

- {item.value ?? "-"} -

-
-
- ))} -
-
-
- - - -
- {t("Server List")} - - {t( - "Inspect individual server health, disk utilization, and network status." - )} - -
- - {t("Total")}: {systemInfo?.servers?.length ?? 0} - -
- - - {(systemInfo?.servers || []).map((server, index) => ( - - -
-
- - - {server.endpoint ?? "--"} - -
-
- - {t("Disks")}:{" "} - {countOnlineDrives(server, "ok")} /{" "} - {server.drives?.length ?? 0} - - - {t("Network")}:{" "} - {countOnlineNetworks(server, "online")} /{" "} - {Object.keys(server.network ?? {}).length} - - - {t("Uptime")}:{" "} - {server.uptime != null - ? dayjs() - .subtract(server.uptime, "second") - .fromNow() - : "--"} - -
-
-
- -

- {t("Version")}: {server.version ?? "--"} -

- -
- {(server.drives || []).map((drive) => ( - - - - {drive.drive_path ?? "--"} - - - {niceBytes(String(drive.usedspace ?? 0))} /{" "} - {niceBytes(String(drive.totalspace ?? 0))} - - - - -
-

- {t("Used")}:{" "} - - {niceBytes( - String(drive.usedspace ?? 0) - )} - -

-

- {t("Available")}:{" "} - - {niceBytes( - String(drive.availspace ?? 0) - )} - -

-
-
-
- ))} -
-
-
-
- ))} -
-
-
+
) diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx new file mode 100644 index 0000000..421fdb5 --- /dev/null +++ b/components/auth/login-form.tsx @@ -0,0 +1,246 @@ +"use client" + +import Link from "next/link" +import Image from "next/image" +import { useTranslation } from "react-i18next" +import { RiSettings3Line } from "@remixicon/react" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Field, FieldContent, FieldLabel } from "@/components/ui/field" +import { ThemeSwitcher } from "@/components/theme-switcher" +import { LanguageSwitcher } from "@/components/language-switcher" +import { AuthHeroStatic } from "@/components/auth/heroes/hero-static" +import { buildRoute } from "@/lib/routes" +import logoImage from "@/assets/logo.svg" + +export type LoginMethod = "accessKeyAndSecretKey" | "sts" + +export interface LoginFormProps { + method: LoginMethod + setMethod: (m: LoginMethod) => void + accessKeyAndSecretKey: { accessKeyId: string; secretAccessKey: string } + setAccessKeyAndSecretKey: React.Dispatch< + React.SetStateAction<{ accessKeyId: string; secretAccessKey: string }> + > + sts: { + accessKeyId: string + secretAccessKey: string + sessionToken: string + } + setSts: React.Dispatch< + React.SetStateAction<{ + accessKeyId: string + secretAccessKey: string + sessionToken: string + }> + > + handleLogin: (e: React.FormEvent) => void +} + +export function LoginForm({ + method, + setMethod, + accessKeyAndSecretKey, + setAccessKeyAndSecretKey, + sts, + setSts, + handleLogin, +}: LoginFormProps) { + const { t } = useTranslation() + + return ( +
+ +
+
+ +
+
+
+ + + +
+ +
+ RustFS + +
+ setMethod(v as LoginMethod)} + className="flex flex-col gap-4" + > + + + {t("Key Login")} + + + {t("STS Login")} + + + +
+
+ {method === "accessKeyAndSecretKey" ? ( + <> + + + {t("Account")} + + + + setAccessKeyAndSecretKey((prev) => ({ + ...prev, + accessKeyId: e.target.value, + })) + } + autoComplete="username" + type="text" + placeholder={t("Please enter account")} + /> + + + + {t("Key")} + + + setAccessKeyAndSecretKey((prev) => ({ + ...prev, + secretAccessKey: e.target.value, + })) + } + autoComplete="current-password" + type="password" + placeholder={t("Please enter key")} + /> + + + + ) : ( + <> + + + {t("STS Username")} + + + + setSts((prev) => ({ + ...prev, + accessKeyId: e.target.value, + })) + } + autoComplete="new-password" + type="text" + placeholder={t("Please enter STS username")} + /> + + + + + {t("STS Key")} + + + + setSts((prev) => ({ + ...prev, + secretAccessKey: e.target.value, + })) + } + autoComplete="new-password" + type="password" + placeholder={t("Please enter STS key")} + /> + + + + + {t("STS Session Token")} + + + + setSts((prev) => ({ + ...prev, + sessionToken: e.target.value, + })) + } + autoComplete="new-password" + type="text" + placeholder={t( + "Please enter STS session token" + )} + /> + + + + )} + + +
+
+
+
+ +
+

+ {t("Login Problems?")}{" "} + + {t("Get Help")} + +

+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ ) +} diff --git a/components/buckets/events-tab.tsx b/components/buckets/events-tab.tsx index 6c1ee3d..cc63fec 100644 --- a/components/buckets/events-tab.tsx +++ b/components/buckets/events-tab.tsx @@ -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 = { - "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 = { - 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[] = React.useMemo( - () => [ - { - id: "type", - header: () => t("Type"), - cell: ({ row }) => ( - - {row.original.type} - - ), - meta: { maxWidth: "7rem" }, - }, - { - id: "arn", - header: () => t("ARN"), - cell: ({ row }) => ( - - {row.original.arn} - - ), - meta: { maxWidth: "180px" }, - }, - { - id: "events", - header: () => t("Events"), - cell: ({ row }) => ( -
- {getDisplayEvents(row.original.events).map((event) => ( - - {event} - - ))} -
- ), - meta: { maxWidth: "13rem" }, - }, - { - id: "prefix", - header: () => t("Prefix"), - cell: ({ row }) => {row.original.prefix || "-"}, - meta: { maxWidth: "9rem" }, - }, - { - id: "suffix", - header: () => t("Suffix"), - cell: ({ row }) => {row.original.suffix || "-"}, - meta: { maxWidth: "9rem" }, - }, - { - id: "actions", - header: () => t("Actions"), - enableSorting: false, - meta: { maxWidth: "6rem" }, - cell: ({ row }) => ( -
- -
- ), - }, - ], - [t] + const handleRowDelete = React.useCallback( + async (row: NotificationItem) => { + const confirmed = await new Promise((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> + )[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({ @@ -218,62 +183,6 @@ export function BucketEventsTab({ bucketName }: BucketEventsTabProps) { getRowId: (row) => row.id, }) - const handleRowDelete = async (row: NotificationItem) => { - const confirmed = await new Promise((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> - )[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 (
diff --git a/components/events/columns.tsx b/components/events/columns.tsx new file mode 100644 index 0000000..18dc004 --- /dev/null +++ b/components/events/columns.tsx @@ -0,0 +1,93 @@ +"use client" + +import { RiDeleteBin7Line } from "@remixicon/react" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { + TYPE_BADGE_CLASSES, + getDisplayEvents, + type NotificationItem, +} from "@/lib/events" +import type { ColumnDef } from "@tanstack/react-table" +import type { TFunction } from "i18next" + +export function getEventsColumns( + t: TFunction, + onDelete: (row: NotificationItem) => void +): ColumnDef[] { + return [ + { + id: "type", + header: () => t("Type"), + cell: ({ row }) => ( + + {row.original.type} + + ), + meta: { maxWidth: "7rem" }, + }, + { + id: "arn", + header: () => t("ARN"), + cell: ({ row }) => ( + + {row.original.arn} + + ), + meta: { maxWidth: "180px" }, + }, + { + id: "events", + header: () => t("Events"), + cell: ({ row }) => ( +
+ {getDisplayEvents(row.original.events).map((event) => ( + + {event} + + ))} +
+ ), + meta: { maxWidth: "13rem" }, + }, + { + id: "prefix", + header: () => t("Prefix"), + cell: ({ row }) => ( + {row.original.prefix ?? "-"} + ), + meta: { maxWidth: "9rem" }, + }, + { + id: "suffix", + header: () => t("Suffix"), + cell: ({ row }) => ( + {row.original.suffix ?? "-"} + ), + meta: { maxWidth: "9rem" }, + }, + { + id: "actions", + header: () => t("Actions"), + enableSorting: false, + meta: { maxWidth: "6rem" }, + cell: ({ row }) => ( +
+ +
+ ), + }, + ] +} diff --git a/components/license/enterprise-section.tsx b/components/license/enterprise-section.tsx new file mode 100644 index 0000000..a3dc3f2 --- /dev/null +++ b/components/license/enterprise-section.tsx @@ -0,0 +1,361 @@ +"use client" + +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { + RiUploadFill, + RiCustomerService2Line, +} from "@remixicon/react" +import dayjs from "dayjs" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { PageHeader } from "@/components/page-header" +import { DataTable } from "@/components/data-table/data-table" +import { useDataTable } from "@/hooks/use-data-table" +import type { ColumnDef } from "@tanstack/react-table" + +const hasValidLicense = false +const endDate = dayjs().format("YYYY-MM-DD") + +interface PermissionItem { + name: string + description: string + status: string +} + +export function LicenseEnterpriseSection() { + const { t } = useTranslation() + + const licenseKey = "RUSTFS-ENTERPRISE-127-183" + + const licenseDetails = useMemo( + () => [ + { label: t("Licensed Company"), value: t("No License") }, + { label: t("License Key"), value: licenseKey }, + { label: t("Licensed Users"), value: t("Unlimited") }, + { + label: t("Support Level"), + value: `${t("Enterprise")} (7x24x365)`, + }, + ], + [t] + ) + + const serviceInfo = useMemo( + () => [ + { label: t("Service Hotline"), value: "400-033-5363" }, + { label: t("Version"), value: "v2.3" }, + { label: t("Service Email"), value: "hello@rustfs.com" }, + { + label: t("Enterprise Service Level"), + value: t("Platinum Service"), + }, + { + label: t("On-site Technical Service"), + value: t("Supported"), + }, + { + label: t("Remote Technical Support"), + value: t("Supported"), + }, + { label: t("Technical Training"), value: t("Supported") }, + { label: t("On-site Deployment"), value: t("Supported") }, + { label: t("Emergency Response"), value: t("Supported") }, + { + label: t("Response Level"), + value: t("One-hour Response"), + }, + ], + [t] + ) + + const permissions = useMemo( + () => [ + { + name: t("Single Machine Multiple Disks"), + description: t( + "Supports managing multiple storage disks on a single server to improve storage resource utilization and simplify management and maintenance" + ), + status: t("Enabled"), + }, + { + name: t("Advanced Monitoring"), + description: t( + "Provides detailed performance monitoring and alerting mechanisms to help administrators understand system status in real-time and ensure system stability and reliability" + ), + status: t("Enabled"), + }, + { + name: t("Metrics"), + description: t( + "Collects and displays key performance indicators (such as CPU, memory, disk I/O, etc.) through visualized charts to help users understand system operation status and performance bottlenecks" + ), + status: t("Enabled"), + }, + ], + [t] + ) + + const permissionsColumns: ColumnDef[] = useMemo( + () => [ + { + id: "name", + header: () => t("Name"), + cell: ({ row }) => ( + {row.original.name} + ), + }, + { + id: "description", + header: () => t("Description"), + cell: ({ row }) => ( + + {row.original.description} + + ), + }, + { + id: "status", + header: () => t("Status"), + cell: ({ row }) => ( + {row.original.status} + ), + }, + ], + [t] + ) + + const { table: permissionsTable } = useDataTable({ + data: permissions, + columns: permissionsColumns, + getRowId: (row) => row.name, + }) + + const technicalParameters = useMemo( + () => [ + { label: t("Service Hotline"), value: "400-033-5363" }, + { + label: t("Supported OS"), + value: "Windows、Linux、MacOS", + }, + { + label: t("Supported CPU Architecture"), + value: "Amd64、ARM、AMR64、MIPS64、S390X、PPC64LE", + }, + { + label: t("Virtualization Platform Support"), + value: "KVM、VMware、Hyper-V、Docker、Kubernetes", + }, + { + label: t("Development Language Requirements"), + value: "C++、Java、Rust、Go、Python、Node.js", + }, + { label: t("SNND Mode"), value: t("Supported") }, + { label: t("SNMD Mode"), value: t("Supported") }, + { label: t("MNMD Mode"), value: t("Supported") }, + { label: t("Bucket Count"), value: t("Unlimited") }, + { label: t("Object Count"), value: t("Unlimited") }, + { + label: t("EC Mode"), + value: t("Reed-Solomon Matrix"), + }, + { label: t("Access Control"), value: "IAM Policy" }, + { + label: t("Secure Transport"), + value: t("Supports HTTPS, TLS"), + }, + { + label: t("Bucket Policy"), + value: t("Public, Private, Custom"), + }, + { label: t("Single Object"), value: t("Max 50TB") }, + { + label: t("Data Redundancy"), + value: t("Supports Erasure Coding"), + }, + { label: t("Data Backup"), value: t("Supported") }, + { label: t("Scalability"), value: t("Supported") }, + { + label: t("Read/Write Performance"), + value: t("Supports high concurrency operations"), + }, + { + label: t("Identity Authentication Expansion"), + value: "OpenID、LDAP", + }, + { label: t("S3 Compatibility"), value: t("Supported") }, + { + label: t("SDK Support"), + value: "Java、Python、Go、Rust、Node.js", + }, + { label: t("Bucket Notification"), value: t("Supported") }, + { label: t("RustyVault Encryption"), value: t("Supported") }, + { label: t("HashiCorp Encryption"), value: t("Supported") }, + { label: t("Lifecycle Management"), value: t("Supported") }, + { label: t("s3fs"), value: t("Supported") }, + { label: t("Prometheus"), value: t("Supported") }, + { label: t("Bucket Quota"), value: t("Supported") }, + { label: t("Audit"), value: t("Supported") }, + { label: t("Logs"), value: t("Supported") }, + { label: t("Object Repair"), value: t("Supported") }, + { label: t("WORM"), value: t("Supported") }, + { label: t("Remote Tiering"), value: t("Supported") }, + { label: t("Tiering Transfer"), value: t("Supported") }, + { label: t("Object Sharing"), value: t("Supported") }, + { label: t("Load Balancing"), value: t("Supported") }, + { label: t("Object Tags"), value: t("Supported") }, + { label: t("Multipart Upload"), value: t("Supported") }, + { label: t("Key Creation"), value: t("Supported") }, + { label: t("Key Expiration"), value: t("Supported") }, + { + label: t("Disk Bad Spot Check"), + value: t("Supported"), + }, + { label: t("Bitrot"), value: t("Supported") }, + { label: t("Version Control"), value: t("Supported") }, + ], + [t] + ) + + const contactSupport = () => { + window.open( + "https://ww18.53kf.com/webCompany.php?arg=11003151&kf_sign=DA4MDMTc0Ng4MjE1MjEzODAyNDkyMDAyNzMwMDMxNTE%253D&style=2", + "_blank" + ) + } + + const updateLicense = () => { + // Placeholder logic + } + + return ( + <> + +

{t("Enterprise License")}

+
+ +
+ + +
+
+
+ + {t("Enterprise License")} + + + {t("Status")}: + {hasValidLicense ? t("Normal") : t("Expired")} + +
+

+ {t("License Valid Until")}:{endDate} +

+
+ +
+ + +
+
+
+
+ +
+ + +

+ {t("License Details")} +

+
+ {licenseDetails.map((item) => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+
+
+ + + +

+ {t("Customer Service")} +

+
+ {serviceInfo.map((item) => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+
+
+
+ + + +

+ {t("Feature Permissions")} +

+ +
+
+ + + +

+ {t("Technical Parameters")} +

+
+ {technicalParameters.map((item) => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+
+
+
+ + ) +} diff --git a/hooks/use-performance-data.ts b/hooks/use-performance-data.ts new file mode 100644 index 0000000..6d68f69 --- /dev/null +++ b/hooks/use-performance-data.ts @@ -0,0 +1,115 @@ +"use client" + +import * as React from "react" +import { useCallback, useEffect, useState } from "react" +import { useTranslation } from "react-i18next" +import { useSystem } from "@/hooks/use-system" + +export interface ServerInfo { + endpoint?: string + state?: string + version?: string + uptime?: number + drives?: Array<{ + uuid?: string + drive_path?: string + usedspace?: number + totalspace?: number + availspace?: number + state?: string + }> + network?: Record +} + +export interface SystemInfo { + buckets?: { count?: number } + objects?: { count?: number } + servers?: ServerInfo[] + backend?: { + backendType?: string + onlineDisks?: number + offlineDisks?: number + } +} + +export interface DataUsageInfo { + total_capacity?: number + total_used_capacity?: number +} + +export interface StorageInfo { + backend?: { + StandardSCParity?: string + RRSCParity?: string + } +} + +export interface MetricsInfo { + aggregated?: { + scanner?: { + current_started?: string + cycle_complete_times?: string[] + } + } +} + +export function usePerformanceData() { + const { t } = useTranslation() + const systemApi = useSystem() + + const [metricsInfo, setMetricsInfo] = useState({}) + const [systemInfo, setSystemInfo] = useState({}) + const [datausageinfo, setDatausageinfo] = useState({}) + const [storageinfo, setStorageinfo] = useState({}) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const mountedRef = React.useRef(true) + + const refetch = useCallback(async () => { + if (!mountedRef.current) return + setLoading(true) + setError(null) + try { + const [sysRes, usageRes, storageRes] = await Promise.all([ + systemApi.getSystemInfo(), + systemApi.getDataUsageInfo(), + systemApi.getStorageInfo(), + ]) + if (!mountedRef.current) return + setSystemInfo((sysRes as SystemInfo) ?? {}) + setDatausageinfo((usageRes as DataUsageInfo) ?? {}) + setStorageinfo((storageRes as StorageInfo) ?? {}) + } catch (err) { + if (!mountedRef.current) return + console.error("Failed to load performance data:", err) + setError((err as Error)?.message ?? t("Get Data Failed")) + } finally { + if (mountedRef.current) setLoading(false) + } + try { + const metricsRes = await systemApi.getSystemMetrics() + if (mountedRef.current) setMetricsInfo((metricsRes as MetricsInfo) ?? {}) + } catch { + if (mountedRef.current) setMetricsInfo({}) + } + }, [systemApi, t]) + + useEffect(() => { + mountedRef.current = true + void refetch() + return () => { + mountedRef.current = false + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- fetch once on mount; systemApi is stable + }, []) + + return { + systemInfo, + metricsInfo, + datausageinfo, + storageinfo, + loading, + error, + refetch, + } +} diff --git a/lib/events.ts b/lib/events.ts new file mode 100644 index 0000000..fad2566 --- /dev/null +++ b/lib/events.ts @@ -0,0 +1,38 @@ +/** + * Events (bucket notifications) shared types, constants and helpers. + */ + +export interface NotificationItem { + id: string + type: "Lambda" | "SQS" | "SNS" | "Topic" + arn: string + events: string[] + prefix?: string + suffix?: string + filterRules?: Array<{ Name: string; Value: string }> +} + +export const EVENT_DISPLAY_MAP: Record = { + "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", +} + +export const TYPE_BADGE_CLASSES: Record = { + 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", +} + +export function getDisplayEvents(events: string[]): string[] { + return [...new Set(events.map((e) => EVENT_DISPLAY_MAP[e] ?? e))] +}