feat: openresty状态上报

This commit is contained in:
ryan
2026-03-12 15:09:34 +08:00
parent ecc71428e4
commit 42ca18681c
26 changed files with 1232 additions and 165 deletions
@@ -61,3 +61,9 @@ export function requestNodeAgentUpdate(
body: JSON.stringify(payload ?? {}),
});
}
export function requestNodeOpenrestyRestart(id: number) {
return apiRequest<NodeItem>(`/nodes/${id}/openresty-restart`, {
method: 'POST',
});
}
@@ -21,6 +21,7 @@ import {
deleteNode,
getNodeAgentRelease,
getNodes,
requestNodeOpenrestyRestart,
requestNodeAgentUpdate,
updateNode,
} from '@/features/nodes/api/nodes';
@@ -45,6 +46,8 @@ import {
getApplyVariant,
getNodeStatusLabel,
getNodeStatusVariant,
getOpenrestyStatusLabel,
getOpenrestyStatusVariant,
getServerUrl,
getUpdateMode,
isMeaningfulTime,
@@ -202,6 +205,20 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
},
});
const restartOpenrestyMutation = useMutation({
mutationFn: () => requestNodeOpenrestyRestart(Number(nodeId)),
onSuccess: async (updatedNode) => {
setFeedback({
tone: 'success',
message: `已向节点 ${updatedNode.name} 下发 OpenResty 重启指令。`,
});
await queryClient.invalidateQueries({ queryKey: nodesQueryKey });
},
onError: (error) => {
setFeedback({ tone: 'danger', message: getErrorMessage(error) });
},
});
const deleteMutation = useMutation({
mutationFn: () => deleteNode(Number(nodeId)),
onSuccess: async () => {
@@ -236,6 +253,23 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
deleteMutation.mutate();
};
const handleRestartOpenresty = () => {
if (!node) {
return;
}
if (
!window.confirm(
`确认向节点“${node.name}”下发 OpenResty 重启指令吗?该指令会在下一次心跳后执行。`,
)
) {
return;
}
setFeedback(null);
restartOpenrestyMutation.mutate();
};
const handleCopy = async (value: string, message: string) => {
try {
await copyToClipboard(value);
@@ -337,6 +371,17 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
>
{node.update_requested ? '查看 Agent 更新' : 'Agent 更新'}
</PrimaryButton>
<SecondaryButton
type="button"
onClick={handleRestartOpenresty}
disabled={restartOpenrestyMutation.isPending}
>
{restartOpenrestyMutation.isPending
? '下发重启中...'
: node.restart_openresty_requested
? '等待 OpenResty 重启'
: '重启 OpenResty'}
</SecondaryButton>
<DangerButton
type="button"
onClick={handleDelete}
@@ -378,6 +423,20 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
</div>
</AppCard>
<AppCard title="OpenResty 健康">
<div className="space-y-3">
<StatusBadge
label={getOpenrestyStatusLabel(node.openresty_status)}
variant={getOpenrestyStatusVariant(node.openresty_status)}
/>
<p className="text-sm text-[var(--foreground-secondary)]">
{node.restart_openresty_requested
? '已等待节点在下一次心跳后执行 OpenResty 重启。'
: node.openresty_message || '当前未上报额外错误。'}
</p>
</div>
</AppCard>
<AppCard title="更新模式">
<div className="space-y-3">
<StatusBadge
@@ -485,6 +544,14 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
{node.last_error || '无'}
</p>
</div>
<div>
<p className="font-medium text-[var(--foreground-primary)]">
OpenResty 状态消息
</p>
<p className="mt-1 break-words whitespace-pre-wrap">
{node.openresty_message || '无'}
</p>
</div>
<div>
<p className="font-medium text-[var(--foreground-primary)]">
创建时间
@@ -36,6 +36,8 @@ import {
getApplyVariant,
getNodeStatusLabel,
getNodeStatusVariant,
getOpenrestyStatusLabel,
getOpenrestyStatusVariant,
isMeaningfulTime,
} from '@/features/nodes/utils';
@@ -236,7 +238,8 @@ export function NodesPage() {
<tr className="text-[var(--foreground-secondary)]">
<th className="px-3 py-3 font-medium">节点</th>
<th className="px-3 py-3 font-medium">状态</th>
<th className="px-3 py-3 font-medium">Agent / Nginx</th>
<th className="px-3 py-3 font-medium">Agent / OpenResty</th>
<th className="px-3 py-3 font-medium">运行健康</th>
<th className="px-3 py-3 font-medium">当前版本</th>
<th className="px-3 py-3 font-medium">最近应用</th>
<th className="px-3 py-3 font-medium">最近心跳</th>
@@ -266,6 +269,21 @@ export function NodesPage() {
{node.agent_version || 'unknown'} /{' '}
{node.nginx_version || 'unknown'}
</td>
<td className="px-3 py-4">
<div className="space-y-2">
<StatusBadge
label={getOpenrestyStatusLabel(
node.openresty_status,
)}
variant={getOpenrestyStatusVariant(
node.openresty_status,
)}
/>
<p className="max-w-64 text-xs break-words whitespace-pre-wrap text-[var(--foreground-secondary)]">
{node.openresty_message || '无额外错误'}
</p>
</div>
</td>
<td className="px-3 py-4 text-[var(--foreground-secondary)]">
{node.current_version || '未应用'}
</td>
+3
View File
@@ -10,8 +10,11 @@ export interface NodeItem {
update_requested: boolean;
update_channel: ReleaseChannel;
update_tag: string;
restart_openresty_requested: boolean;
agent_version: string;
nginx_version: string;
openresty_status: 'healthy' | 'unhealthy' | 'unknown';
openresty_message: string;
status: 'online' | 'offline' | 'pending';
current_version: string;
last_seen_at: string;
+24
View File
@@ -68,6 +68,30 @@ export function getUpdateMode(node: NodeItem) {
return { label: '手动更新', variant: 'info' as const };
}
export function getOpenrestyStatusVariant(status: NodeItem['openresty_status']) {
if (status === 'healthy') {
return 'success';
}
if (status === 'unhealthy') {
return 'danger';
}
return 'warning';
}
export function getOpenrestyStatusLabel(status: NodeItem['openresty_status']) {
if (status === 'healthy') {
return '健康';
}
if (status === 'unhealthy') {
return '异常';
}
return '未知';
}
function parseVersionParts(version: string) {
const normalized = version.trim().replace(/^v/i, '');
if (!normalized || normalized.toLowerCase() === 'unknown') {