refactor(servers): unify connection status badges
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
Deploy to Production / Trigger production update (push) Has been cancelled

Extract ServerStatusBadge so every server list renders the auth-error
state identically instead of repeating the online/offline ternary, and
expose last_error_kind from the cluster topology and user-assignment
endpoints, which build their payloads by hand and so did not inherit it
from the server schema.
This commit is contained in:
markmorado
2026-07-24 05:04:45 +05:00
parent ebc692a324
commit e10f9a2a03
9 changed files with 40 additions and 45 deletions

View File

@@ -89,6 +89,7 @@ def get_cluster_topology(
"ip_address": s.ip_address,
"port": s.port,
"is_online": s.is_online,
"last_error_kind": s.last_error_kind,
"cluster_name": s.cluster_name,
}
if s.cluster_name:

View File

@@ -654,6 +654,7 @@ async def get_user_server_assignments(
"name": server.name,
"ip_address": server.ip_address,
"is_online": server.is_online,
"last_error_kind": server.last_error_kind,
"workspaces": server_workspaces,
"compatible": compatible,
"assigned": server.id in assigned_ids,

View File

@@ -17,6 +17,7 @@ import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { StatusDot } from '@/components/shared/status-dot';
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
import { formatBytes } from '@/lib/format';
import {
useClusterTopology, useCreateCluster, usePreJoinCheck, usePrepareJoin,
@@ -521,10 +522,11 @@ export default function ClusterPage() {
</div>
</div>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<StatusDot status={m.is_online ? 'online' : 'offline'} pulse />
{m.is_online ? t('common.online', 'Online') : t('common.offline', 'Offline')}
</span>
<ServerStatusBadge
isOnline={m.is_online}
errorKind={m.last_error_kind}
variant="inline"
/>
<Button
variant="ghost"
size="icon"
@@ -566,10 +568,11 @@ export default function ClusterPage() {
<p className="text-xs text-muted-foreground font-mono truncate">{s.ip_address}</p>
</div>
</div>
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<StatusDot status={s.is_online ? 'online' : 'offline'} pulse />
{s.is_online ? t('common.online', 'Online') : t('common.offline', 'Offline')}
</span>
<ServerStatusBadge
isOnline={s.is_online}
errorKind={s.last_error_kind}
variant="inline"
/>
</div>
))}
</div>

View File

@@ -14,6 +14,7 @@ import { useServer, useNodes, useClusterInfo } from '@/hooks/use-nodes';
import { useVirtualMachines } from '@/hooks/use-instances';
import { useNodePower } from '@/hooks/use-node-admin';
import { useConfirm } from '@/components/shared/ConfirmDialog';
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
import { useAuthStore } from '@/stores/auth-store';
import { StatusDot } from '@/components/shared/status-dot';
import { formatBytes, formatUptime, formatPercent } from '@/lib/format';
@@ -116,21 +117,11 @@ export default function NodeDetailPage() {
: t('nodes.standalone')}
</Badge>
)}
<Badge
variant={
server?.is_online ? 'default'
: server?.last_error_kind === 'auth' ? 'outline'
: 'destructive'
}
className={server?.last_error_kind === 'auth' && !server?.is_online
? 'border-warning/40 text-warning' : undefined}
title={server?.last_error_kind === 'auth' && !server?.is_online
? t('nodes.auth_error_hint') : server?.last_error || undefined}
>
{server?.is_online ? t('common.online')
: server?.last_error_kind === 'auth' ? t('nodes.auth_error')
: t('common.offline')}
</Badge>
<ServerStatusBadge
isOnline={server?.is_online}
errorKind={server?.last_error_kind}
lastError={server?.last_error}
/>
</div>
</div>

View File

@@ -13,10 +13,9 @@ import { useServers, useCreateServer, useUpdateServer, useDeleteServer, useTestS
import { useVirtualMachines } from '@/hooks/use-instances';
import type { ProxmoxServer, ProxmoxServerCreate } from '@/types';
import { formatUptime } from '@/lib/format';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { useConfirm } from '@/components/shared/ConfirmDialog';
import { StatusDot } from '@/components/shared/status-dot';
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
import ServerUpdatesBadge from './ServerUpdatesBadge';
/**
@@ -359,8 +358,6 @@ export default function NodesPage() {
{servers.map(srv => {
const srvVMs = vms.filter(v => v.server_id === srv.id && v.type === 'qemu');
const srvCTs = vms.filter(v => v.server_id === srv.id && v.type === 'lxc');
// Сервер ответил 401 — он жив, дело в учётных данных, а не в связи.
const authError = !srv.is_online && srv.last_error_kind === 'auth';
return (
<Card key={srv.id} className="relative group">
<CardContent className="p-5 space-y-4">
@@ -449,18 +446,11 @@ export default function NodesPage() {
<span className="font-mono tabular-nums">{srvCTs.length} LXC</span>
</div>
</div>
<Badge
variant={srv.is_online ? 'default' : authError ? 'outline' : 'destructive'}
className={cn('gap-1.5', authError && 'border-warning/40 text-warning')}
title={authError ? t('nodes.auth_error_hint') : srv.last_error || undefined}
>
<StatusDot
status={srv.is_online ? 'online' : authError ? 'auth_error' : 'offline'}
pulse
className="h-1.5 w-1.5"
/>
{srv.is_online ? t('common.online') : authError ? t('nodes.auth_error') : t('common.offline')}
</Badge>
<ServerStatusBadge
isOnline={srv.is_online}
errorKind={srv.last_error_kind}
lastError={srv.last_error}
/>
</div>
<ServerMeta serverId={srv.id} online={srv.is_online} />

View File

@@ -12,6 +12,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
@@ -639,9 +640,11 @@ function UserServersForm({
)}
</div>
<div className="flex flex-col items-end gap-1 shrink-0">
<Badge variant={srv.is_online ? 'default' : 'destructive'} className="text-xs">
{srv.is_online ? t('common.online') : t('common.offline')}
</Badge>
<ServerStatusBadge
isOnline={srv.is_online}
errorKind={srv.last_error_kind}
className="text-xs"
/>
{!srv.compatible && (
<Badge variant="outline" className="text-xs">{t('users.incompatible')}</Badge>
)}

View File

@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogClose, DialogFooter } from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useWorkspaces, useCreateWorkspace, useUpdateWorkspace, useDeleteWorkspace, useWorkspace, useUpdateWorkspaceServers, useUpdateWorkspaceUsers } from '@/hooks/use-workspaces';
@@ -204,9 +205,12 @@ function EditWorkspaceDialog({
<p className="text-sm font-medium">{srv.name}</p>
<p className="text-xs text-muted-foreground font-mono">{srv.ip_address}</p>
</div>
<Badge variant={srv.is_online ? 'default' : 'destructive'} className="ml-auto shrink-0 text-xs">
{srv.is_online ? t('common.online') : t('common.offline')}
</Badge>
<ServerStatusBadge
isOnline={srv.is_online}
errorKind={srv.last_error_kind}
lastError={srv.last_error}
className="ml-auto shrink-0 text-xs"
/>
</label>
))}
</div>

View File

@@ -12,6 +12,7 @@ export interface ClusterServerEntry {
ip_address: string;
port: number;
is_online: boolean;
last_error_kind?: 'auth' | 'connection' | null;
cluster_name: string | null;
}

View File

@@ -77,6 +77,7 @@ export interface ServerAssignment {
name: string;
ip_address: string;
is_online: boolean;
last_error_kind?: 'auth' | 'connection' | null;
workspaces: { id: number; name: string }[];
compatible: boolean;
assigned: boolean;