Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Before committing any code changes, you MUST run and pass:

## UI Theme Overrides

- For every Console UI, interaction, settings, form, dialog, table, responsive-layout, or visual-review change, read and follow `skills/rustfs-console-design-guide/SKILL.md` before editing. Use `skills/ui-audit/SKILL.md` as the audit workflow and the Console design guide as the source of design decisions.
- Apply visual tweaks (e.g. removing shadows, altering colors) at usage sites via classes such as `class="shadow-none"`.
- When extending shadcn components, create wrapper components (e.g. `BucketSelector.tsx`) instead of forking primitives.
- Do not change base colors or theme variables defined in `console-new` unless explicitly required by the migration plan.
Expand Down
48 changes: 25 additions & 23 deletions app/(auth)/config/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,29 +102,31 @@ function ConfigPageContent() {
}

return (
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-muted p-4 sm:p-6 lg:p-20">
<div className="relative flex min-h-dvh flex-col items-center justify-center overflow-hidden bg-muted p-4 sm:p-8 lg:p-12">
<Image
src={buildRoute("/backgrounds/scillate.svg")}
alt=""
fill
priority
sizes="100vw"
className="absolute inset-0 z-0 opacity-45 object-cover"
className="absolute inset-0 z-0 object-cover opacity-30"
/>
<div className="z-10 mx-auto flex w-full max-w-7xl flex-col overflow-hidden border bg-card shadow-none lg:min-h-[560px] lg:flex-row">
<div className="hidden w-1/2 lg:block">
<div className="z-10 mx-auto flex w-full max-w-6xl flex-col overflow-hidden border bg-card shadow-none lg:min-h-[600px] lg:flex-row">
<div className="hidden lg:block lg:w-5/12">
<AuthHeroStatic />
</div>
<div className="flex w-full flex-col items-center justify-center bg-card lg:w-1/2">
<div className="max-w-sm w-full space-y-6 p-4 sm:p-7">
<ThemeLogo width={112} height={24} priority />
<div className="py-6">
<h1 className="block text-2xl font-bold text-foreground">{t("Server Configuration")}</h1>
<div className="flex w-full flex-col items-center justify-center bg-card lg:w-7/12">
<div className="w-full max-w-md p-6 sm:p-8 lg:p-10">
<ThemeLogo width={112} height={24} className="lg:hidden" priority />
<div className="mt-8 lg:mt-0">
<h1 className="block font-heading text-2xl font-semibold tracking-tight text-foreground">
{t("Server Configuration")}
</h1>
<p className="mt-2 text-sm text-muted-foreground">{t("Please configure your RustFS server address")}</p>
</div>

<div className="mt-5 space-y-4">
<form onSubmit={validateAndSave} autoComplete="off">
<div className="mt-8 space-y-4">
<form onSubmit={validateAndSave} autoComplete="off" aria-busy={isSaving}>
<div className="grid gap-y-6">
<Field>
<FieldLabel htmlFor="serverHost">{t("Server Address")}</FieldLabel>
Expand All @@ -138,6 +140,7 @@ function ConfigPageContent() {
type="url"
autoComplete="off"
spellCheck={false}
className="h-11 text-base sm:h-8 sm:text-xs"
placeholder={t("Please enter server address (e.g., http://localhost:9000)")}
/>
</FieldContent>
Expand All @@ -147,14 +150,18 @@ function ConfigPageContent() {
</Field>

<div className="flex flex-col gap-3 sm:flex-row">
<Button type="submit" className="w-full sm:flex-1" disabled={isSaving}>
<Button
type="submit"
className="h-11 w-full text-sm sm:h-8 sm:flex-1 sm:text-xs"
disabled={isSaving}
>
{t("Save Configuration")}
</Button>

<Button
type="button"
variant="outline"
className="w-full sm:w-auto"
className="h-11 w-full text-sm sm:h-8 sm:w-auto sm:text-xs"
onClick={resetToCurrentHost}
disabled={isSaving}
>
Expand All @@ -163,8 +170,8 @@ function ConfigPageContent() {

<Button
type="button"
variant="outline"
className="w-full sm:w-auto"
variant="ghost"
className="h-11 w-full text-sm sm:h-8 sm:w-auto sm:text-xs"
onClick={skipConfig}
disabled={isSaving}
>
Expand All @@ -175,20 +182,15 @@ function ConfigPageContent() {
</form>
</div>

<div className="my-8">
<p className="text-sm text-muted-foreground">
<div className="mt-8 flex items-center justify-between gap-4 border-t pt-5">
<p className="min-w-0 text-sm text-muted-foreground">
{t("Need help?")}{" "}
<Link href={docs} className="text-foreground underline-offset-4 hover:underline">
{t("View Documentation")}
</Link>
</p>
</div>

<div className="mx-auto flex w-1/2 items-center justify-around gap-4">
<div className="inline-flex">
<div className="flex shrink-0 items-center gap-1 [&_[data-slot=button]]:size-10 sm:[&_[data-slot=button]]:size-8">
<ThemeSwitcher />
</div>
<div className="inline-flex">
<LanguageSwitcher />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<main id="main-content" tabIndex={-1} className="min-h-screen min-w-0">
<main id="main-content" tabIndex={-1} className="min-h-dvh min-w-0">
{children}
</main>
)
Expand Down
37 changes: 20 additions & 17 deletions app/(dashboard)/_components/performance-backend-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card"

export interface BackendInfoItem {
icon: React.ComponentType<{ className?: string; "aria-hidden"?: boolean | "true" | "false" }>
Expand All @@ -11,26 +11,29 @@ export interface BackendInfoItem {
export function PerformanceBackendCard({ items, t }: { items: BackendInfoItem[]; t: (key: string) => string }) {
return (
<Card className="shadow-none">
<CardHeader>
<CardTitle>{items.length ? t("Backend Services") : ""}</CardTitle>
<CardDescription>
{items.length ? t("Key services and configuration values reported by the cluster.") : ""}
</CardDescription>
<CardHeader className="pb-3">
<h2 id="storage-configuration-title" className="text-base font-semibold">
{t("Storage Configuration")}
</h2>
<CardDescription>{t("Parity and backend settings reported by the cluster.")}</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<dl className="grid gap-0 border-y md:grid-cols-3" aria-labelledby="storage-configuration-title">
{items.map((item) => (
<Card key={item.title} className="border bg-muted/40 shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{item.title}</CardTitle>
<item.icon className="size-5 text-muted-foreground" aria-hidden />
</CardHeader>
<CardContent>
<p className="text-xl font-semibold text-foreground">{item.value ?? "-"}</p>
</CardContent>
</Card>
<div
key={item.title}
className="flex min-w-0 items-center gap-3 border-b py-4 last:border-b-0 md:border-e md:border-b-0 md:px-4 md:first:ps-0 md:last:border-e-0 md:last:pe-0"
>
<item.icon className="size-5 shrink-0 text-muted-foreground" aria-hidden />
<div className="min-w-0">
<dt className="text-xs text-muted-foreground">{item.title}</dt>
<dd className="mt-1 break-words text-base font-semibold text-foreground">
{item.value ?? t("Unknown")}
</dd>
</div>
</div>
))}
</div>
</dl>
</CardContent>
</Card>
)
Expand Down
142 changes: 91 additions & 51 deletions app/(dashboard)/_components/performance-infrastructure-card.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,112 @@
"use client"

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card"

function HealthMetric({ label, value, unavailableLabel }: { label: string; value?: number; unavailableLabel: string }) {
return (
<div className="min-w-0 border-t pt-3">
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className="mt-1 text-lg font-semibold tabular-nums">{value ?? unavailableLabel}</dd>
</div>
)
}

export function PerformanceInfrastructureCard({
onlineServers,
offlineServers,
unknownServers,
degradedServers,
initializingServers,
unknownServers,
onlineDisks,
offlineDisks,
unknownDisks,
t,
}: {
onlineServers: number
offlineServers: number
unknownServers: number
degradedServers: number
onlineDisks: number
offlineDisks: number
unknownDisks: number
onlineServers?: number
offlineServers?: number
degradedServers?: number
initializingServers?: number
unknownServers?: number
onlineDisks?: number
offlineDisks?: number
unknownDisks?: number
t: (key: string) => string
}) {
const needsAttention = Boolean((offlineServers ?? 0) + (degradedServers ?? 0) + (offlineDisks ?? 0))
const hasUnknownState = Boolean((initializingServers ?? 0) + (unknownServers ?? 0) + (unknownDisks ?? 0))
const serverCounts = [onlineServers, offlineServers, degradedServers, initializingServers, unknownServers]
const diskCounts = [onlineDisks, offlineDisks, unknownDisks]
const dataUnavailable =
serverCounts.some((value) => value === undefined) ||
diskCounts.some((value) => value === undefined) ||
serverCounts.reduce<number>((total, value) => total + (value ?? 0), 0) === 0 ||
diskCounts.reduce<number>((total, value) => total + (value ?? 0), 0) === 0
const status = needsAttention
? t("Needs attention")
: hasUnknownState || dataUnavailable
? t("Unknown")
: t("Healthy")
const description = needsAttention
? t("Offline or degraded resources require attention.")
: hasUnknownState || dataUnavailable
? t("Some resource health data is unavailable or still initializing.")
: t("All reported servers and disks are online.")

return (
<Card className="shadow-none">
<Card className="h-full shadow-none">
<CardHeader className="pb-3">
<CardTitle>{t("Infrastructure Health")}</CardTitle>
<CardDescription>{t("Real-time status of cluster servers and backend storage devices.")}</CardDescription>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1.5">
<h2 id="cluster-health-title" className="text-base font-semibold">
{t("Cluster Health")}
</h2>
<CardDescription>{description}</CardDescription>
</div>
<Badge
variant={needsAttention ? "destructive" : hasUnknownState || dataUnavailable ? "outline" : "secondary"}
aria-live="polite"
>
{status}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid gap-4 lg:grid-cols-2">
<div className="border bg-muted/40 p-4">
<p className="text-sm font-medium text-muted-foreground">{t("Servers")}</p>
<div className="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<div className="border bg-background p-3">
<p className="text-xs text-muted-foreground">{t("Online")}</p>
<p className="mt-1 text-xl font-semibold text-foreground">{onlineServers}</p>
</div>
<div className="border bg-background p-3">
<p className="text-xs text-muted-foreground">{t("Offline")}</p>
<p className="mt-1 text-xl font-semibold text-foreground">{offlineServers}</p>
</div>
<div className="border bg-background p-3">
<p className="text-xs text-muted-foreground">{t("Unknown")}</p>
<p className="mt-1 text-xl font-semibold text-foreground">{unknownServers}</p>
</div>
<div className="border bg-background p-3">
<p className="text-xs text-muted-foreground">{t("Degraded")}</p>
<p className="mt-1 text-xl font-semibold text-foreground">{degradedServers}</p>
</div>
</div>
</div>
<div className="border bg-muted/40 p-4">
<p className="text-sm font-medium text-muted-foreground">{t("Disks")}</p>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<div className="border bg-background p-3">
<p className="text-xs text-muted-foreground">{t("Online")}</p>
<p className="mt-1 text-xl font-semibold text-foreground">{onlineDisks}</p>
</div>
<div className="border bg-background p-3">
<p className="text-xs text-muted-foreground">{t("Offline")}</p>
<p className="mt-1 text-xl font-semibold text-foreground">{offlineDisks}</p>
</div>
<div className="border bg-background p-3">
<p className="text-xs text-muted-foreground">{t("Unknown")}</p>
<p className="mt-1 text-xl font-semibold text-foreground">{unknownDisks}</p>
</div>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2" aria-labelledby="cluster-health-title">
<section aria-labelledby="server-health-title">
<h3 id="server-health-title" className="text-sm font-medium text-muted-foreground">
{t("Servers")}
</h3>
<dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-3">
<HealthMetric label={t("Online")} value={onlineServers} unavailableLabel={t("Unavailable")} />
<HealthMetric label={t("Offline")} value={offlineServers} unavailableLabel={t("Unavailable")} />
{degradedServers ? (
<HealthMetric label={t("Degraded")} value={degradedServers} unavailableLabel={t("Unavailable")} />
) : null}
{initializingServers ? (
<HealthMetric
label={t("Initializing")}
value={initializingServers}
unavailableLabel={t("Unavailable")}
/>
) : null}
{unknownServers ? (
<HealthMetric label={t("Unknown")} value={unknownServers} unavailableLabel={t("Unavailable")} />
) : null}
</dl>
</section>
<section aria-labelledby="disk-health-title">
<h3 id="disk-health-title" className="text-sm font-medium text-muted-foreground">
{t("Disks")}
</h3>
<dl className="mt-3 grid grid-cols-3 gap-x-4 gap-y-3">
<HealthMetric label={t("Online")} value={onlineDisks} unavailableLabel={t("Unavailable")} />
<HealthMetric label={t("Offline")} value={offlineDisks} unavailableLabel={t("Unavailable")} />
{unknownDisks ? (
<HealthMetric label={t("Unknown")} value={unknownDisks} unavailableLabel={t("Unavailable")} />
) : null}
</dl>
</section>
</div>
</CardContent>
</Card>
Expand Down
Loading