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,
|
"ip_address": s.ip_address,
|
||||||
"port": s.port,
|
"port": s.port,
|
||||||
"is_online": s.is_online,
|
"is_online": s.is_online,
|
||||||
|
"last_error_kind": s.last_error_kind,
|
||||||
"cluster_name": s.cluster_name,
|
"cluster_name": s.cluster_name,
|
||||||
}
|
}
|
||||||
if s.cluster_name:
|
if s.cluster_name:
|
||||||
|
|||||||
@@ -654,6 +654,7 @@ async def get_user_server_assignments(
|
|||||||
"name": server.name,
|
"name": server.name,
|
||||||
"ip_address": server.ip_address,
|
"ip_address": server.ip_address,
|
||||||
"is_online": server.is_online,
|
"is_online": server.is_online,
|
||||||
|
"last_error_kind": server.last_error_kind,
|
||||||
"workspaces": server_workspaces,
|
"workspaces": server_workspaces,
|
||||||
"compatible": compatible,
|
"compatible": compatible,
|
||||||
"assigned": server.id in assigned_ids,
|
"assigned": server.id in assigned_ids,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { StatusDot } from '@/components/shared/status-dot';
|
import { StatusDot } from '@/components/shared/status-dot';
|
||||||
|
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
|
||||||
import { formatBytes } from '@/lib/format';
|
import { formatBytes } from '@/lib/format';
|
||||||
import {
|
import {
|
||||||
useClusterTopology, useCreateCluster, usePreJoinCheck, usePrepareJoin,
|
useClusterTopology, useCreateCluster, usePreJoinCheck, usePrepareJoin,
|
||||||
@@ -521,10 +522,11 @@ export default function ClusterPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<ServerStatusBadge
|
||||||
<StatusDot status={m.is_online ? 'online' : 'offline'} pulse />
|
isOnline={m.is_online}
|
||||||
{m.is_online ? t('common.online', 'Online') : t('common.offline', 'Offline')}
|
errorKind={m.last_error_kind}
|
||||||
</span>
|
variant="inline"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -566,10 +568,11 @@ export default function ClusterPage() {
|
|||||||
<p className="text-xs text-muted-foreground font-mono truncate">{s.ip_address}</p>
|
<p className="text-xs text-muted-foreground font-mono truncate">{s.ip_address}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
<ServerStatusBadge
|
||||||
<StatusDot status={s.is_online ? 'online' : 'offline'} pulse />
|
isOnline={s.is_online}
|
||||||
{s.is_online ? t('common.online', 'Online') : t('common.offline', 'Offline')}
|
errorKind={s.last_error_kind}
|
||||||
</span>
|
variant="inline"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useServer, useNodes, useClusterInfo } from '@/hooks/use-nodes';
|
|||||||
import { useVirtualMachines } from '@/hooks/use-instances';
|
import { useVirtualMachines } from '@/hooks/use-instances';
|
||||||
import { useNodePower } from '@/hooks/use-node-admin';
|
import { useNodePower } from '@/hooks/use-node-admin';
|
||||||
import { useConfirm } from '@/components/shared/ConfirmDialog';
|
import { useConfirm } from '@/components/shared/ConfirmDialog';
|
||||||
|
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
import { StatusDot } from '@/components/shared/status-dot';
|
import { StatusDot } from '@/components/shared/status-dot';
|
||||||
import { formatBytes, formatUptime, formatPercent } from '@/lib/format';
|
import { formatBytes, formatUptime, formatPercent } from '@/lib/format';
|
||||||
@@ -116,21 +117,11 @@ export default function NodeDetailPage() {
|
|||||||
: t('nodes.standalone')}
|
: t('nodes.standalone')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Badge
|
<ServerStatusBadge
|
||||||
variant={
|
isOnline={server?.is_online}
|
||||||
server?.is_online ? 'default'
|
errorKind={server?.last_error_kind}
|
||||||
: server?.last_error_kind === 'auth' ? 'outline'
|
lastError={server?.last_error}
|
||||||
: '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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ import { useServers, useCreateServer, useUpdateServer, useDeleteServer, useTestS
|
|||||||
import { useVirtualMachines } from '@/hooks/use-instances';
|
import { useVirtualMachines } from '@/hooks/use-instances';
|
||||||
import type { ProxmoxServer, ProxmoxServerCreate } from '@/types';
|
import type { ProxmoxServer, ProxmoxServerCreate } from '@/types';
|
||||||
import { formatUptime } from '@/lib/format';
|
import { formatUptime } from '@/lib/format';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useConfirm } from '@/components/shared/ConfirmDialog';
|
import { useConfirm } from '@/components/shared/ConfirmDialog';
|
||||||
import { StatusDot } from '@/components/shared/status-dot';
|
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
|
||||||
import ServerUpdatesBadge from './ServerUpdatesBadge';
|
import ServerUpdatesBadge from './ServerUpdatesBadge';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -359,8 +358,6 @@ export default function NodesPage() {
|
|||||||
{servers.map(srv => {
|
{servers.map(srv => {
|
||||||
const srvVMs = vms.filter(v => v.server_id === srv.id && v.type === 'qemu');
|
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');
|
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 (
|
return (
|
||||||
<Card key={srv.id} className="relative group">
|
<Card key={srv.id} className="relative group">
|
||||||
<CardContent className="p-5 space-y-4">
|
<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>
|
<span className="font-mono tabular-nums">{srvCTs.length} LXC</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<ServerStatusBadge
|
||||||
variant={srv.is_online ? 'default' : authError ? 'outline' : 'destructive'}
|
isOnline={srv.is_online}
|
||||||
className={cn('gap-1.5', authError && 'border-warning/40 text-warning')}
|
errorKind={srv.last_error_kind}
|
||||||
title={authError ? t('nodes.auth_error_hint') : srv.last_error || undefined}
|
lastError={srv.last_error}
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ServerMeta serverId={srv.id} online={srv.is_online} />
|
<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 { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ServerStatusBadge } from '@/components/shared/ServerStatusBadge';
|
||||||
import {
|
import {
|
||||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
@@ -639,9 +640,11 @@ function UserServersForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-1 shrink-0">
|
<div className="flex flex-col items-end gap-1 shrink-0">
|
||||||
<Badge variant={srv.is_online ? 'default' : 'destructive'} className="text-xs">
|
<ServerStatusBadge
|
||||||
{srv.is_online ? t('common.online') : t('common.offline')}
|
isOnline={srv.is_online}
|
||||||
</Badge>
|
errorKind={srv.last_error_kind}
|
||||||
|
className="text-xs"
|
||||||
|
/>
|
||||||
{!srv.compatible && (
|
{!srv.compatible && (
|
||||||
<Badge variant="outline" className="text-xs">{t('users.incompatible')}</Badge>
|
<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 { Label } from '@/components/ui/label';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogClose, DialogFooter } from '@/components/ui/dialog';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useWorkspaces, useCreateWorkspace, useUpdateWorkspace, useDeleteWorkspace, useWorkspace, useUpdateWorkspaceServers, useUpdateWorkspaceUsers } from '@/hooks/use-workspaces';
|
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-sm font-medium">{srv.name}</p>
|
||||||
<p className="text-xs text-muted-foreground font-mono">{srv.ip_address}</p>
|
<p className="text-xs text-muted-foreground font-mono">{srv.ip_address}</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={srv.is_online ? 'default' : 'destructive'} className="ml-auto shrink-0 text-xs">
|
<ServerStatusBadge
|
||||||
{srv.is_online ? t('common.online') : t('common.offline')}
|
isOnline={srv.is_online}
|
||||||
</Badge>
|
errorKind={srv.last_error_kind}
|
||||||
|
lastError={srv.last_error}
|
||||||
|
className="ml-auto shrink-0 text-xs"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface ClusterServerEntry {
|
|||||||
ip_address: string;
|
ip_address: string;
|
||||||
port: number;
|
port: number;
|
||||||
is_online: boolean;
|
is_online: boolean;
|
||||||
|
last_error_kind?: 'auth' | 'connection' | null;
|
||||||
cluster_name: string | null;
|
cluster_name: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export interface ServerAssignment {
|
|||||||
name: string;
|
name: string;
|
||||||
ip_address: string;
|
ip_address: string;
|
||||||
is_online: boolean;
|
is_online: boolean;
|
||||||
|
last_error_kind?: 'auth' | 'connection' | null;
|
||||||
workspaces: { id: number; name: string }[];
|
workspaces: { id: number; name: string }[];
|
||||||
compatible: boolean;
|
compatible: boolean;
|
||||||
assigned: boolean;
|
assigned: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user