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
26 changes: 22 additions & 4 deletions console-web/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Spinner } from "@/components/ui/spinner"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import * as api from "@/lib/api"
import { ApiError } from "@/lib/api-client"
import { normalizeTopologyTenantState } from "@/lib/tenant-state"
import { cn, formatBinaryBytes, formatK8sMemory } from "@/lib/utils"
import type { ClusterResourcesResponse, NamespaceItem, NodeInfo } from "@/types/api"
import type { TopologyOverviewResponse, TopologyTenantState } from "@/types/topology"
Expand All @@ -33,6 +34,16 @@ const STATE_THEME: Record<
dot: "bg-emerald-500",
card: "border-emerald-200 bg-emerald-50/60",
},
Reconciling: {
badge: "border-blue-200 bg-blue-50 text-blue-700",
dot: "bg-blue-500",
card: "border-blue-200 bg-blue-50/60",
},
Blocked: {
badge: "border-purple-200 bg-purple-50 text-purple-700",
dot: "bg-purple-500",
card: "border-purple-200 bg-purple-50/60",
},
Updating: {
badge: "border-blue-200 bg-blue-50 text-blue-700",
dot: "bg-blue-500",
Expand Down Expand Up @@ -60,8 +71,11 @@ function getTreeDotClass(state: string): string {
case "Ready":
case "Running":
return "bg-emerald-500"
case "Reconciling":
case "Updating":
return "bg-blue-500"
case "Blocked":
return "bg-purple-500"
case "Degraded":
case "Pending":
return "bg-amber-500"
Expand Down Expand Up @@ -151,7 +165,10 @@ export default function DashboardPage() {
const topologySummary = topology?.cluster.summary
const tenantCount = topology?.namespaces.reduce((sum, ns) => sum + ns.tenants.length, 0) ?? 0
const unhealthyCount =
topology?.namespaces.reduce((sum, ns) => sum + ns.tenants.filter((t) => t.state !== "Ready").length, 0) ?? 0
topology?.namespaces.reduce(
(sum, ns) => sum + ns.tenants.filter((tenant) => normalizeTopologyTenantState(tenant.state) !== "Ready").length,
0,
) ?? 0

const allPods = topology?.namespaces.flatMap((ns) => ns.tenants.flatMap((t) => t.pods ?? [])) ?? []
const podTotal = allPods.length
Expand Down Expand Up @@ -257,17 +274,18 @@ export default function DashboardPage() {
const tenantId = `t:${ns.name}/${tenant.name}`
const pools = tenant.pools ?? []
const pods = tenant.pods ?? []
const tenantState = normalizeTopologyTenantState(tenant.state)
return (
<div key={tenant.name} className="vtree-vnode">
<button
type="button"
className="vtree-vbox vtree-vbox--tenant"
onClick={() => toggleTreeNode(tenantId)}
>
<span className={cn("vtree-vdot", getTreeDotClass(tenant.state))} />
<span className={cn("vtree-vdot", getTreeDotClass(tenantState))} />
<span className="vtree-vbox-name">{tenant.name}</span>
<span className={cn("vtree-vstate", STATE_THEME[tenant.state].badge)}>
{t(tenant.state)}
<span className={cn("vtree-vstate", STATE_THEME[tenantState].badge)}>
{t(tenantState)}
</span>
<RiArrowDownSLine
className={cn("vtree-vchevron", !isTreeExpanded(tenantId) && "rotate-180")}
Expand Down
32 changes: 18 additions & 14 deletions console-web/app/(dashboard)/tenants/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import * as api from "@/lib/api"
import { ApiError } from "@/lib/api-client"
import { routes } from "@/lib/routes"
import { normalizeTenantLifecycleState } from "@/lib/tenant-state"
import { parseSizeToBytes, formatBinaryBytes } from "@/lib/utils"
import type { ServiceInfo, TenantLifecycleState, TenantListItem, TenantStateCountsResponse } from "@/types/api"

const ALL_NAMESPACES = "__all__"
const TENANT_STATES: TenantLifecycleState[] = ["Ready", "Updating", "Degraded", "NotReady", "Unknown"]
const TENANT_STATES: TenantLifecycleState[] = ["Ready", "Reconciling", "Blocked", "Degraded", "NotReady", "Unknown"]
const EMPTY_STATE_COUNTS: Record<TenantLifecycleState, number> = {
Ready: 0,
Reconciling: 0,
Blocked: 0,
Updating: 0,
Degraded: 0,
NotReady: 0,
Expand All @@ -59,6 +62,18 @@ const STATE_THEME: Record<
label: "text-emerald-700",
activeCard: "border-emerald-300 ring-1 ring-emerald-200",
},
Reconciling: {
badge: "bg-blue-50 text-blue-700 border-blue-200",
dot: "bg-blue-500",
label: "text-blue-700",
activeCard: "border-blue-300 ring-1 ring-blue-200",
},
Blocked: {
badge: "bg-purple-50 text-purple-700 border-purple-200",
dot: "bg-purple-500",
label: "text-purple-700",
activeCard: "border-purple-300 ring-1 ring-purple-200",
},
Updating: {
badge: "bg-blue-50 text-blue-700 border-blue-200",
dot: "bg-blue-500",
Expand Down Expand Up @@ -96,18 +111,7 @@ function makeTenantKey(namespace: string, name: string): string {
return `${namespace}/${name}`
}

function normalizeTenantState(state: string | null | undefined): TenantLifecycleState {
const normalized = (state ?? "")
.trim()
.toLowerCase()
.replace(/[\s_-]/g, "")
if (normalized === "ready" || normalized === "running") return "Ready"
if (normalized === "updating" || normalized.includes("provision")) return "Updating"
if (normalized === "degraded") return "Degraded"
if (normalized === "notready" || normalized === "error" || normalized.includes("fail")) return "NotReady"
if (normalized === "unknown" || normalized === "stopped") return "Unknown"
return "Unknown"
}
const normalizeTenantState = normalizeTenantLifecycleState

function parseStateCounts(payload: TenantStateCountsResponse): Record<TenantLifecycleState, number> {
const result: Record<TenantLifecycleState, number> = { ...EMPTY_STATE_COUNTS }
Expand Down Expand Up @@ -392,7 +396,7 @@ export default function TenantsListPage() {
<p className="text-sm text-muted-foreground">{t("Manage RustFS tenant instances.")}</p>
</PageHeader>

<div className="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-6">
<div className="mt-4 grid gap-3 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-7">
<button
type="button"
onClick={() => setSelectedState(null)}
Expand Down
2 changes: 2 additions & 0 deletions console-web/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
"Endpoint is unavailable": "Endpoint is unavailable",
"Copy failed": "Copy failed",
"Status": "Status",
"Reconciling": "Reconciling",
"Blocked": "Blocked",
"Updating": "Updating",
"Degraded": "Degraded",
"NotReady": "NotReady",
Expand Down
2 changes: 2 additions & 0 deletions console-web/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
"Endpoint is unavailable": "Endpoint 不可用",
"Copy failed": "复制失败",
"Status": "状态",
"Reconciling": "协调中",
"Blocked": "阻塞",
"Updating": "更新中",
"Degraded": "降级",
"NotReady": "未就绪",
Expand Down
34 changes: 34 additions & 0 deletions console-web/lib/tenant-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { TenantLifecycleState } from "@/types/api"
import type { TopologyTenantState } from "@/types/topology"

function normalizeStateToken(state: string | null | undefined): string {
return (state ?? "")
.trim()
.toLowerCase()
.replace(/[\s_-]/g, "")
}

export function normalizeTenantLifecycleState(state: string | null | undefined): TenantLifecycleState {
const normalized = normalizeStateToken(state)

if (normalized === "ready" || normalized === "running") return "Ready"
if (
normalized === "reconciling" ||
normalized === "updating" ||
normalized === "creating" ||
normalized.includes("provision")
) {
return "Reconciling"
}
if (normalized === "blocked") return "Blocked"
if (normalized === "degraded") return "Degraded"
if (normalized === "notready" || normalized === "error" || normalized.includes("fail")) {
return "NotReady"
}
if (normalized === "unknown" || normalized === "stopped") return "Unknown"
return "Unknown"
}

export function normalizeTopologyTenantState(state: string | null | undefined): TopologyTenantState {
return normalizeTenantLifecycleState(state)
}
40 changes: 39 additions & 1 deletion console-web/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,28 @@ export interface TenantListItem {
namespace: string
pools: PoolInfo[]
state: string
ready?: boolean
reconciling?: boolean
degraded?: boolean
primary_reason?: string | null
generation?: number | null
observed_generation?: number | null
stale?: boolean
created_at: string | null
}

export interface TenantListResponse {
tenants: TenantListItem[]
}

export type TenantLifecycleState = "Ready" | "Updating" | "Degraded" | "NotReady" | "Unknown"
export type TenantLifecycleState =
| "Ready"
| "Reconciling"
| "Blocked"
| "Updating"
| "Degraded"
| "NotReady"
| "Unknown"

export interface TenantStateCountItem {
state: string
Expand All @@ -46,11 +60,35 @@ export interface ServiceInfo {
ports: ServicePort[]
}

export interface TenantCondition {
type: string
status: string
reason: string
message: string
last_transition_time: string | null
observed_generation: number | null
}

export interface TenantStatusSummary {
current_state: string
ready: boolean
reconciling: boolean
degraded: boolean
primary_reason: string | null
primary_message: string | null
observed_generation: number | null
stale: boolean
next_actions: string[]
}

export interface TenantDetailsResponse {
name: string
namespace: string
pools: PoolInfo[]
state: string
status_summary: TenantStatusSummary
conditions: TenantCondition[]
next_actions: string[]
image: string | null
mount_path: string | null
created_at: string | null
Expand Down
7 changes: 6 additions & 1 deletion console-web/types/topology.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type TopologyTenantState = "Ready" | "Updating" | "Degraded" | "NotReady" | "Unknown"
export type TopologyTenantState = "Ready" | "Reconciling" | "Blocked" | "Updating" | "Degraded" | "NotReady" | "Unknown"

export interface TopologyClusterSummary {
nodes: number
Expand Down Expand Up @@ -48,6 +48,11 @@ export interface TopologyTenant {
name: string
namespace: string
state: TopologyTenantState
ready?: boolean
reconciling?: boolean
degraded?: boolean
stale?: boolean
primary_reason?: string | null
created_at: string | null
summary: TopologyTenantSummary
pools?: TopologyPool[]
Expand Down
Loading
Loading