refactor(servers): unify connection status badges
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:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user