mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(proxy-nodes): 简化批量升级为直接写入 upgrade_to 目标
- 后端移除分波 rollout/探测逻辑,改为一次性给所有合格 tunnel 节点写入升级目标,并自动取消活动中的 rollout - 前端移除 rollout 进度、阶段筛选、重试/跳过/冲突清理等相关 UI 和交互 - 同步更新批量升级接口类型与测试用例
This commit is contained in:
@@ -6,7 +6,7 @@ use crate::handlers::admin::shared::query_param_value;
|
|||||||
use crate::maintenance::{
|
use crate::maintenance::{
|
||||||
cancel_proxy_upgrade_rollout, clear_proxy_upgrade_rollout_conflicts,
|
cancel_proxy_upgrade_rollout, clear_proxy_upgrade_rollout_conflicts,
|
||||||
restore_proxy_upgrade_rollout_skipped_nodes, retry_proxy_upgrade_rollout_node,
|
restore_proxy_upgrade_rollout_skipped_nodes, retry_proxy_upgrade_rollout_node,
|
||||||
skip_proxy_upgrade_rollout_node, start_proxy_upgrade_rollout, ProxyUpgradeRolloutProbeConfig,
|
skip_proxy_upgrade_rollout_node, ProxyUpgradeRolloutProbeConfig,
|
||||||
};
|
};
|
||||||
use crate::GatewayError;
|
use crate::GatewayError;
|
||||||
use aether_admin::system::{
|
use aether_admin::system::{
|
||||||
@@ -130,6 +130,16 @@ struct ProxyNodeBatchUpgradeRequest {
|
|||||||
probe_timeout_secs: Option<u64>,
|
probe_timeout_secs: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ProxyNodeBatchUpgradeDispatchSummary {
|
||||||
|
version: String,
|
||||||
|
eligible_total: usize,
|
||||||
|
updated: usize,
|
||||||
|
skipped: usize,
|
||||||
|
node_ids: Vec<String>,
|
||||||
|
rollout_cancelled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
const JSON_OBJECT_REQUIRED_DETAIL: &str = "请求体必须是合法的 JSON 对象";
|
const JSON_OBJECT_REQUIRED_DETAIL: &str = "请求体必须是合法的 JSON 对象";
|
||||||
const DEFAULT_PROXY_UPGRADE_BATCH_SIZE: usize = 1;
|
const DEFAULT_PROXY_UPGRADE_BATCH_SIZE: usize = 1;
|
||||||
const DEFAULT_PROXY_UPGRADE_COOLDOWN_SECS: u64 = 60;
|
const DEFAULT_PROXY_UPGRADE_COOLDOWN_SECS: u64 = 60;
|
||||||
@@ -506,10 +516,7 @@ pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
|||||||
if decision.route_kind.as_deref() == Some("batch_upgrade_nodes")
|
if decision.route_kind.as_deref() == Some("batch_upgrade_nodes")
|
||||||
&& request_context.method() == http::Method::POST
|
&& request_context.method() == http::Method::POST
|
||||||
{
|
{
|
||||||
if !state.has_proxy_node_reader()
|
if !state.has_proxy_node_reader() || !state.has_proxy_node_writer() {
|
||||||
|| !state.has_proxy_node_writer()
|
|
||||||
|| !state.app().data.has_system_config_store()
|
|
||||||
{
|
|
||||||
return Ok(Some(build_admin_proxy_nodes_data_unavailable_response()));
|
return Ok(Some(build_admin_proxy_nodes_data_unavailable_response()));
|
||||||
}
|
}
|
||||||
let input = match parse_json_body::<ProxyNodeBatchUpgradeRequest>(request_body) {
|
let input = match parse_json_body::<ProxyNodeBatchUpgradeRequest>(request_body) {
|
||||||
@@ -520,42 +527,16 @@ pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
|||||||
Ok(version) => version,
|
Ok(version) => version,
|
||||||
Err(response) => return Ok(Some(response)),
|
Err(response) => return Ok(Some(response)),
|
||||||
};
|
};
|
||||||
let batch_size = match validate_batch_size(input.batch_size) {
|
let summary = dispatch_proxy_node_upgrade_targets(state, &version).await?;
|
||||||
Ok(batch_size) => batch_size,
|
|
||||||
Err(response) => return Ok(Some(response)),
|
|
||||||
};
|
|
||||||
let cooldown_secs = match validate_cooldown_secs(input.cooldown_secs) {
|
|
||||||
Ok(cooldown_secs) => cooldown_secs,
|
|
||||||
Err(response) => return Ok(Some(response)),
|
|
||||||
};
|
|
||||||
let probe =
|
|
||||||
match validate_probe_config(input.probe_url.as_deref(), input.probe_timeout_secs) {
|
|
||||||
Ok(probe) => probe,
|
|
||||||
Err(response) => return Ok(Some(response)),
|
|
||||||
};
|
|
||||||
let rollout = start_proxy_upgrade_rollout(
|
|
||||||
&state.app().data,
|
|
||||||
version.clone(),
|
|
||||||
batch_size,
|
|
||||||
cooldown_secs,
|
|
||||||
probe,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|err| GatewayError::Internal(err.to_string()))?;
|
|
||||||
|
|
||||||
return Ok(Some(
|
return Ok(Some(
|
||||||
Json(json!({
|
Json(json!({
|
||||||
"version": version,
|
"version": summary.version,
|
||||||
"batch_size": rollout.batch_size,
|
"eligible_total": summary.eligible_total,
|
||||||
"cooldown_secs": rollout.cooldown_secs,
|
"updated": summary.updated,
|
||||||
"updated": rollout.updated,
|
"skipped": summary.skipped,
|
||||||
"skipped": rollout.skipped,
|
"node_ids": summary.node_ids,
|
||||||
"node_ids": rollout.node_ids,
|
"rollout_cancelled": summary.rollout_cancelled,
|
||||||
"blocked": rollout.blocked,
|
|
||||||
"pending_node_ids": rollout.pending_node_ids,
|
|
||||||
"rollout_active": rollout.rollout_active,
|
|
||||||
"completed": rollout.completed,
|
|
||||||
"remaining": rollout.remaining,
|
|
||||||
}))
|
}))
|
||||||
.into_response(),
|
.into_response(),
|
||||||
));
|
));
|
||||||
@@ -1480,6 +1461,79 @@ fn admin_proxy_node_test_node_id_from_path(path: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_proxy_upgrade_version(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.strip_prefix("proxy-v")
|
||||||
|
.unwrap_or(value.trim())
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dispatch_proxy_node_upgrade_targets(
|
||||||
|
state: &AdminAppState<'_>,
|
||||||
|
version: &str,
|
||||||
|
) -> Result<ProxyNodeBatchUpgradeDispatchSummary, GatewayError> {
|
||||||
|
let mut nodes = state.list_proxy_nodes().await?;
|
||||||
|
nodes.sort_by(|left, right| left.name.cmp(&right.name).then(left.id.cmp(&right.id)));
|
||||||
|
|
||||||
|
let rollout_cancelled = if state.app().data.has_system_config_store() {
|
||||||
|
cancel_proxy_upgrade_rollout(&state.app().data)
|
||||||
|
.await
|
||||||
|
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||||
|
.is_some()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let normalized_target = normalize_proxy_upgrade_version(version);
|
||||||
|
let mut summary = ProxyNodeBatchUpgradeDispatchSummary {
|
||||||
|
version: version.to_string(),
|
||||||
|
rollout_cancelled,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for node in nodes {
|
||||||
|
if node.is_manual || !node.tunnel_mode {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.eligible_total = summary.eligible_total.saturating_add(1);
|
||||||
|
|
||||||
|
let current_version =
|
||||||
|
aether_data::repository::proxy_nodes::proxy_reported_version(node.proxy_metadata.as_ref());
|
||||||
|
let pending_target = aether_data::repository::proxy_nodes::remote_config_upgrade_target(
|
||||||
|
node.remote_config.as_ref(),
|
||||||
|
);
|
||||||
|
if pending_target.as_deref() == Some(normalized_target.as_str())
|
||||||
|
|| current_version.as_deref() == Some(normalized_target.as_str())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(updated) = state
|
||||||
|
.update_proxy_node_remote_config(
|
||||||
|
&aether_data::repository::proxy_nodes::ProxyNodeRemoteConfigMutation {
|
||||||
|
node_id: node.id.clone(),
|
||||||
|
node_name: None,
|
||||||
|
allowed_ports: None,
|
||||||
|
log_level: None,
|
||||||
|
heartbeat_interval: None,
|
||||||
|
scheduling_state: None,
|
||||||
|
upgrade_to: Some(Some(version.to_string())),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
summary.node_ids.push(updated.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.updated = summary.node_ids.len();
|
||||||
|
summary.skipped = summary.eligible_total.saturating_sub(summary.updated);
|
||||||
|
Ok(summary)
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_batch_size(batch_size: Option<usize>) -> Result<usize, Response<Body>> {
|
fn validate_batch_size(batch_size: Option<usize>) -> Result<usize, Response<Body>> {
|
||||||
let batch_size = batch_size.unwrap_or(DEFAULT_PROXY_UPGRADE_BATCH_SIZE);
|
let batch_size = batch_size.unwrap_or(DEFAULT_PROXY_UPGRADE_BATCH_SIZE);
|
||||||
if (1..=100).contains(&batch_size) {
|
if (1..=100).contains(&batch_size) {
|
||||||
|
|||||||
@@ -1252,7 +1252,7 @@ async fn gateway_handles_admin_proxy_node_events_locally_with_trusted_admin_prin
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_updates_proxy_node_config_and_batches_upgrade_locally() {
|
async fn gateway_updates_proxy_node_config_and_dispatches_upgrade_targets_locally() {
|
||||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||||
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
let upstream_hits_clone = Arc::clone(&upstream_hits);
|
||||||
let upstream = Router::new()
|
let upstream = Router::new()
|
||||||
@@ -1346,15 +1346,14 @@ async fn gateway_updates_proxy_node_config_and_batches_upgrade_locally() {
|
|||||||
.await
|
.await
|
||||||
.expect("json body should parse");
|
.expect("json body should parse");
|
||||||
assert_eq!(upgrade_payload["version"], "2.0.0");
|
assert_eq!(upgrade_payload["version"], "2.0.0");
|
||||||
assert_eq!(upgrade_payload["batch_size"], 1);
|
assert_eq!(upgrade_payload["eligible_total"], 3);
|
||||||
assert_eq!(upgrade_payload["updated"], 1);
|
assert_eq!(upgrade_payload["updated"], 3);
|
||||||
assert_eq!(upgrade_payload["skipped"], 1);
|
assert_eq!(upgrade_payload["skipped"], 0);
|
||||||
assert_eq!(upgrade_payload["blocked"], false);
|
assert_eq!(
|
||||||
assert_eq!(upgrade_payload["pending_node_ids"], json!(["node-online"]));
|
upgrade_payload["node_ids"],
|
||||||
assert_eq!(upgrade_payload["node_ids"], json!(["node-online"]));
|
json!(["node-online", "node-offline", "node-zeta"])
|
||||||
assert_eq!(upgrade_payload["completed"], 0);
|
);
|
||||||
assert_eq!(upgrade_payload["remaining"], 2);
|
assert_eq!(upgrade_payload["rollout_cancelled"], false);
|
||||||
assert_eq!(upgrade_payload["rollout_active"], true);
|
|
||||||
|
|
||||||
let blocked_upgrade_response = client
|
let blocked_upgrade_response = client
|
||||||
.post(format!("{gateway_url}/api/admin/proxy-nodes/upgrade"))
|
.post(format!("{gateway_url}/api/admin/proxy-nodes/upgrade"))
|
||||||
@@ -1372,11 +1371,7 @@ async fn gateway_updates_proxy_node_config_and_batches_upgrade_locally() {
|
|||||||
.await
|
.await
|
||||||
.expect("json body should parse");
|
.expect("json body should parse");
|
||||||
assert_eq!(blocked_upgrade_payload["updated"], 0);
|
assert_eq!(blocked_upgrade_payload["updated"], 0);
|
||||||
assert_eq!(blocked_upgrade_payload["blocked"], true);
|
assert_eq!(blocked_upgrade_payload["skipped"], 3);
|
||||||
assert_eq!(
|
|
||||||
blocked_upgrade_payload["pending_node_ids"],
|
|
||||||
json!(["node-online"])
|
|
||||||
);
|
|
||||||
|
|
||||||
let heartbeat_response = client
|
let heartbeat_response = client
|
||||||
.post(format!("{gateway_url}/api/internal/tunnel/heartbeat"))
|
.post(format!("{gateway_url}/api/internal/tunnel/heartbeat"))
|
||||||
@@ -1404,7 +1399,7 @@ async fn gateway_updates_proxy_node_config_and_batches_upgrade_locally() {
|
|||||||
assert_eq!(heartbeat_payload["remote_config"]["allowed_ports"][1], 8443);
|
assert_eq!(heartbeat_payload["remote_config"]["allowed_ports"][1], 8443);
|
||||||
assert_eq!(heartbeat_payload["remote_config"]["log_level"], "info");
|
assert_eq!(heartbeat_payload["remote_config"]["log_level"], "info");
|
||||||
|
|
||||||
let second_upgrade_response = client
|
let post_heartbeat_upgrade_response = client
|
||||||
.post(format!("{gateway_url}/api/admin/proxy-nodes/upgrade"))
|
.post(format!("{gateway_url}/api/admin/proxy-nodes/upgrade"))
|
||||||
.header(GATEWAY_HEADER, "rust-phase3b")
|
.header(GATEWAY_HEADER, "rust-phase3b")
|
||||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
@@ -1414,57 +1409,14 @@ async fn gateway_updates_proxy_node_config_and_batches_upgrade_locally() {
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("request should succeed");
|
.expect("request should succeed");
|
||||||
assert_eq!(second_upgrade_response.status(), StatusCode::OK);
|
assert_eq!(post_heartbeat_upgrade_response.status(), StatusCode::OK);
|
||||||
let second_upgrade_payload: serde_json::Value = second_upgrade_response
|
let post_heartbeat_upgrade_payload: serde_json::Value = post_heartbeat_upgrade_response
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.expect("json body should parse");
|
.expect("json body should parse");
|
||||||
assert_eq!(second_upgrade_payload["batch_size"], 1);
|
assert_eq!(post_heartbeat_upgrade_payload["node_ids"], json!([]));
|
||||||
assert_eq!(second_upgrade_payload["updated"], 0);
|
assert_eq!(post_heartbeat_upgrade_payload["updated"], 0);
|
||||||
assert_eq!(second_upgrade_payload["skipped"], 2);
|
assert_eq!(post_heartbeat_upgrade_payload["skipped"], 3);
|
||||||
assert_eq!(second_upgrade_payload["blocked"], true);
|
|
||||||
assert_eq!(
|
|
||||||
second_upgrade_payload["pending_node_ids"],
|
|
||||||
json!(["node-online"])
|
|
||||||
);
|
|
||||||
assert_eq!(second_upgrade_payload["node_ids"], json!([]));
|
|
||||||
assert_eq!(second_upgrade_payload["completed"], 0);
|
|
||||||
assert_eq!(second_upgrade_payload["remaining"], 3);
|
|
||||||
assert_eq!(second_upgrade_payload["rollout_active"], true);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
record_proxy_upgrade_traffic_success(&data_state, "node-online")
|
|
||||||
.await
|
|
||||||
.expect("traffic confirmation should be recorded")
|
|
||||||
);
|
|
||||||
|
|
||||||
let third_upgrade_response = client
|
|
||||||
.post(format!("{gateway_url}/api/admin/proxy-nodes/upgrade"))
|
|
||||||
.header(GATEWAY_HEADER, "rust-phase3b")
|
|
||||||
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
|
||||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
|
||||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
|
||||||
.json(&json!({ "version": "2.0.0", "cooldown_secs": 0 }))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.expect("request should succeed");
|
|
||||||
assert_eq!(third_upgrade_response.status(), StatusCode::OK);
|
|
||||||
let third_upgrade_payload: serde_json::Value = third_upgrade_response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.expect("json body should parse");
|
|
||||||
assert_eq!(third_upgrade_payload["batch_size"], 1);
|
|
||||||
assert_eq!(third_upgrade_payload["updated"], 1);
|
|
||||||
assert_eq!(third_upgrade_payload["skipped"], 1);
|
|
||||||
assert_eq!(third_upgrade_payload["blocked"], false);
|
|
||||||
assert_eq!(
|
|
||||||
third_upgrade_payload["pending_node_ids"],
|
|
||||||
json!(["node-zeta"])
|
|
||||||
);
|
|
||||||
assert_eq!(third_upgrade_payload["node_ids"], json!(["node-zeta"]));
|
|
||||||
assert_eq!(third_upgrade_payload["completed"], 1);
|
|
||||||
assert_eq!(third_upgrade_payload["remaining"], 1);
|
|
||||||
assert_eq!(third_upgrade_payload["rollout_active"], true);
|
|
||||||
|
|
||||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||||
|
|
||||||
@@ -1473,7 +1425,7 @@ async fn gateway_updates_proxy_node_config_and_batches_upgrade_locally() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_marks_draining_proxy_nodes_unschedulable_and_rollout_skips_them() {
|
async fn gateway_still_dispatches_upgrade_targets_to_draining_proxy_nodes() {
|
||||||
let mut alpha = sample_proxy_node("node-alpha");
|
let mut alpha = sample_proxy_node("node-alpha");
|
||||||
alpha.name = "alpha".to_string();
|
alpha.name = "alpha".to_string();
|
||||||
alpha.status = "online".to_string();
|
alpha.status = "online".to_string();
|
||||||
@@ -1562,11 +1514,11 @@ async fn gateway_marks_draining_proxy_nodes_unschedulable_and_rollout_skips_them
|
|||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.expect("json body should parse");
|
.expect("json body should parse");
|
||||||
assert_eq!(upgrade_payload["updated"], 1);
|
assert_eq!(upgrade_payload["eligible_total"], 2);
|
||||||
assert_eq!(upgrade_payload["node_ids"], json!(["node-zeta"]));
|
assert_eq!(upgrade_payload["updated"], 2);
|
||||||
assert_eq!(upgrade_payload["pending_node_ids"], json!(["node-zeta"]));
|
assert_eq!(upgrade_payload["node_ids"], json!(["node-alpha", "node-zeta"]));
|
||||||
|
|
||||||
let rollout_response = client
|
let list_response_after_upgrade = client
|
||||||
.get(format!(
|
.get(format!(
|
||||||
"{gateway_url}/api/admin/proxy-nodes?skip=0&limit=10"
|
"{gateway_url}/api/admin/proxy-nodes?skip=0&limit=10"
|
||||||
))
|
))
|
||||||
@@ -1577,11 +1529,17 @@ async fn gateway_marks_draining_proxy_nodes_unschedulable_and_rollout_skips_them
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect("request should succeed");
|
.expect("request should succeed");
|
||||||
let rollout_payload: serde_json::Value = rollout_response
|
let list_payload_after_upgrade: serde_json::Value = list_response_after_upgrade
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.expect("json body should parse");
|
.expect("json body should parse");
|
||||||
assert_eq!(rollout_payload["rollout"]["online_eligible_total"], 1);
|
let alpha_after_upgrade = list_payload_after_upgrade["items"]
|
||||||
|
.as_array()
|
||||||
|
.expect("items should be array")
|
||||||
|
.iter()
|
||||||
|
.find(|item| item["id"] == "node-alpha")
|
||||||
|
.expect("alpha should exist after upgrade");
|
||||||
|
assert_eq!(alpha_after_upgrade["remote_config"]["upgrade_to"], "2.0.0");
|
||||||
|
|
||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,18 +126,11 @@ export interface ProxyNodeTestResult {
|
|||||||
|
|
||||||
export interface ProxyNodeBatchUpgradeResult {
|
export interface ProxyNodeBatchUpgradeResult {
|
||||||
version: string
|
version: string
|
||||||
batch_size: number
|
eligible_total: number
|
||||||
cooldown_secs: number
|
|
||||||
probe_url?: string | null
|
|
||||||
probe_timeout_secs?: number | null
|
|
||||||
updated: number
|
updated: number
|
||||||
skipped: number
|
skipped: number
|
||||||
node_ids: string[]
|
node_ids: string[]
|
||||||
blocked: boolean
|
rollout_cancelled: boolean
|
||||||
pending_node_ids: string[]
|
|
||||||
rollout_active: boolean
|
|
||||||
completed: number
|
|
||||||
remaining: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProxyNodeUpgradeRolloutCancelResult {
|
export interface ProxyNodeUpgradeRolloutCancelResult {
|
||||||
|
|||||||
@@ -63,25 +63,6 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<Select
|
|
||||||
v-model="filterRolloutStage"
|
|
||||||
:disabled="!store.rollout"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="w-full h-8 text-xs border-border/60">
|
|
||||||
<SelectValue placeholder="阶段" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem
|
|
||||||
v-for="option in rolloutStageOptions"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,23 +98,6 @@
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select
|
|
||||||
v-model="filterRolloutStage"
|
|
||||||
:disabled="!store.rollout"
|
|
||||||
>
|
|
||||||
<SelectTrigger class="w-36 h-8 text-xs border-border/60">
|
|
||||||
<SelectValue placeholder="全部阶段" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem
|
|
||||||
v-for="option in rolloutStageOptions"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div class="h-4 w-px bg-border" />
|
<div class="h-4 w-px bg-border" />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -160,305 +124,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="store.rollout"
|
|
||||||
class="px-4 sm:px-6 py-4 border-b border-border/60 bg-muted/15"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
:variant="store.rollout.blocked ? 'warning' : 'success'"
|
|
||||||
class="text-[11px] px-2 py-0.5"
|
|
||||||
>
|
|
||||||
{{ store.rollout.blocked ? 'Rollout 等待确认' : 'Rollout 推进中' }}
|
|
||||||
</Badge>
|
|
||||||
<span class="text-sm font-semibold">目标版本 {{ store.rollout.version }}</span>
|
|
||||||
<span class="text-xs text-muted-foreground">
|
|
||||||
每波 {{ store.rollout.batch_size }} 个节点 · 冷却 {{ formatDurationShort(store.rollout.cooldown_secs) }}
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
v-if="filterRolloutStage !== 'all'"
|
|
||||||
variant="outline"
|
|
||||||
class="text-[11px] px-2 py-0.5"
|
|
||||||
>
|
|
||||||
筛选: {{ activeRolloutStageLabel }}
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
v-if="filterRolloutStage !== 'all'"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-7 px-2 text-[11px]"
|
|
||||||
@click="filterRolloutStage = 'all'"
|
|
||||||
>
|
|
||||||
清除阶段筛选
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
|
||||||
<div class="rounded-lg border border-border/50 bg-background/80 px-3 py-2">
|
|
||||||
<div class="text-[11px] text-muted-foreground">
|
|
||||||
已完成
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-sm font-semibold tabular-nums">
|
|
||||||
{{ store.rollout.completed_node_ids.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-border/50 bg-background/80 px-3 py-2">
|
|
||||||
<div class="text-[11px] text-muted-foreground">
|
|
||||||
等待确认
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-sm font-semibold tabular-nums">
|
|
||||||
{{ store.rollout.pending_node_ids.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-border/50 bg-background/80 px-3 py-2">
|
|
||||||
<div class="text-[11px] text-muted-foreground">
|
|
||||||
冲突节点
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-sm font-semibold tabular-nums">
|
|
||||||
{{ store.rollout.conflict_node_ids.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-border/50 bg-background/80 px-3 py-2">
|
|
||||||
<div class="text-[11px] text-muted-foreground">
|
|
||||||
已跳过
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-sm font-semibold tabular-nums">
|
|
||||||
{{ store.rollout.skipped_node_ids.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-lg border border-border/50 bg-background/80 px-3 py-2">
|
|
||||||
<div class="text-[11px] text-muted-foreground">
|
|
||||||
在线可升级
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-sm font-semibold tabular-nums">
|
|
||||||
{{ store.rollout.online_eligible_total }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="rolloutPendingPreview.length > 0"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<div class="text-xs font-medium text-foreground/80">
|
|
||||||
当前卡点
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<div
|
|
||||||
v-for="entry in rolloutPendingPreview"
|
|
||||||
:key="entry.node_id"
|
|
||||||
class="flex items-center gap-2 rounded-full border border-border/60 bg-background/90 px-3 py-1 text-xs"
|
|
||||||
>
|
|
||||||
<span class="font-medium">{{ entry.name }}</span>
|
|
||||||
<Badge
|
|
||||||
:variant="entry.variant"
|
|
||||||
class="px-1.5 py-0 text-[10px]"
|
|
||||||
>
|
|
||||||
{{ entry.label }}
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
v-if="canRetryUpgradeNode(entry.node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 px-2 text-[11px]"
|
|
||||||
:disabled="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
@click="handleRetryUpgradeNode(entry.node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
class="h-3 w-3 mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<RotateCcw
|
|
||||||
v-else
|
|
||||||
class="h-3 w-3 mr-1"
|
|
||||||
/>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="canSkipUpgradeNode(entry.node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 px-2 text-[11px]"
|
|
||||||
:disabled="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
@click="handleSkipUpgradeNode(entry.node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
class="h-3 w-3 mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<SkipForward
|
|
||||||
v-else
|
|
||||||
class="h-3 w-3 mr-1"
|
|
||||||
/>
|
|
||||||
跳过
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-if="hiddenPendingNodeCount > 0"
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
还有 {{ hiddenPendingNodeCount }} 个节点处于等待状态。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="rolloutConflictPreview.length > 0"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<div class="text-xs font-medium text-foreground/80">
|
|
||||||
冲突节点
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<div
|
|
||||||
v-for="entry in rolloutConflictPreview"
|
|
||||||
:key="entry.node_id"
|
|
||||||
class="flex items-center gap-2 rounded-full border border-border/60 bg-background/90 px-3 py-1 text-xs"
|
|
||||||
>
|
|
||||||
<span class="font-medium">{{ entry.name }}</span>
|
|
||||||
<Badge
|
|
||||||
variant="destructive"
|
|
||||||
class="px-1.5 py-0 text-[10px]"
|
|
||||||
>
|
|
||||||
版本冲突
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
v-if="canRetryUpgradeNode(entry.node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 px-2 text-[11px]"
|
|
||||||
:disabled="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
@click="handleRetryUpgradeNode(entry.node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
class="h-3 w-3 mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<RotateCcw
|
|
||||||
v-else
|
|
||||||
class="h-3 w-3 mr-1"
|
|
||||||
/>
|
|
||||||
对齐版本
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="canSkipUpgradeNode(entry.node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 px-2 text-[11px]"
|
|
||||||
:disabled="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
@click="handleSkipUpgradeNode(entry.node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
class="h-3 w-3 mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<SkipForward
|
|
||||||
v-else
|
|
||||||
class="h-3 w-3 mr-1"
|
|
||||||
/>
|
|
||||||
跳过
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-if="hiddenConflictNodeCount > 0"
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
还有 {{ hiddenConflictNodeCount }} 个节点存在版本冲突。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="rolloutSkippedPreview.length > 0"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<div class="text-xs font-medium text-foreground/80">
|
|
||||||
已跳过节点
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<div
|
|
||||||
v-for="entry in rolloutSkippedPreview"
|
|
||||||
:key="entry.node_id"
|
|
||||||
class="flex items-center gap-2 rounded-full border border-border/60 bg-background/90 px-3 py-1 text-xs"
|
|
||||||
>
|
|
||||||
<span class="font-medium">{{ entry.name }}</span>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
class="px-1.5 py-0 text-[10px]"
|
|
||||||
>
|
|
||||||
已跳过
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
v-if="canRetryUpgradeNode(entry.node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 px-2 text-[11px]"
|
|
||||||
:disabled="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
@click="handleRetryUpgradeNode(entry.node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(entry.node_id)"
|
|
||||||
class="h-3 w-3 mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<RotateCcw
|
|
||||||
v-else
|
|
||||||
class="h-3 w-3 mr-1"
|
|
||||||
/>
|
|
||||||
恢复
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
v-if="hiddenSkippedNodeCount > 0"
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
>
|
|
||||||
还有 {{ hiddenSkippedNodeCount }} 个节点已被人工跳过。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1 text-xs text-muted-foreground xl:text-right">
|
|
||||||
<div class="flex items-center justify-start gap-2 xl:justify-end">
|
|
||||||
<Button
|
|
||||||
v-if="store.rollout.skipped_node_ids.length > 0"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="h-8 text-xs"
|
|
||||||
:disabled="restoringSkippedUpgradeNodes || clearingUpgradeConflicts || cancellingUpgradeRollout"
|
|
||||||
@click="handleRestoreSkippedUpgradeNodes"
|
|
||||||
>
|
|
||||||
{{ restoringSkippedUpgradeNodes ? '恢复中...' : '恢复已跳过' }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="store.rollout.conflict_node_ids.length > 0"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
class="h-8 text-xs"
|
|
||||||
:disabled="clearingUpgradeConflicts || cancellingUpgradeRollout || restoringSkippedUpgradeNodes"
|
|
||||||
@click="handleClearUpgradeConflicts"
|
|
||||||
>
|
|
||||||
{{ clearingUpgradeConflicts ? '清理中...' : '清空冲突' }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
class="h-8 text-xs"
|
|
||||||
:disabled="cancellingUpgradeRollout || clearingUpgradeConflicts || restoringSkippedUpgradeNodes"
|
|
||||||
@click="handleCancelUpgradeRollout"
|
|
||||||
>
|
|
||||||
{{ cancellingUpgradeRollout ? '取消中...' : '取消 Rollout' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>开始时间: {{ formatTime(store.rollout.started_at) }}</div>
|
|
||||||
<div>最近放量: {{ formatTime(store.rollout.last_dispatched_at) }}</div>
|
|
||||||
<div>最近更新: {{ formatTime(store.rollout.updated_at) }}</div>
|
|
||||||
<div v-if="store.rollout.probe">
|
|
||||||
主动探测: GET {{ store.rollout.probe.url }} · {{ store.rollout.probe.timeout_secs }}s
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
主动探测: 未配置,等待真实流量确认
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 桌面端表格 -->
|
<!-- 桌面端表格 -->
|
||||||
<div class="hidden xl:block overflow-x-auto">
|
<div class="hidden xl:block overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -618,42 +283,6 @@
|
|||||||
>
|
>
|
||||||
<History class="h-4 w-4" />
|
<History class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
v-if="canRetryUpgradeNode(node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
:title="isUpgradeNodeActioning(node.id) ? '处理中...' : '重试当前节点升级'"
|
|
||||||
:disabled="isUpgradeNodeActioning(node.id)"
|
|
||||||
@click="handleRetryUpgradeNode(node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(node.id)"
|
|
||||||
class="h-4 w-4 animate-spin"
|
|
||||||
/>
|
|
||||||
<RotateCcw
|
|
||||||
v-else
|
|
||||||
class="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="canSkipUpgradeNode(node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="h-8 w-8"
|
|
||||||
:title="isUpgradeNodeActioning(node.id) ? '处理中...' : '跳过当前节点'"
|
|
||||||
:disabled="isUpgradeNodeActioning(node.id)"
|
|
||||||
@click="handleSkipUpgradeNode(node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(node.id)"
|
|
||||||
class="h-4 w-4 animate-spin"
|
|
||||||
/>
|
|
||||||
<SkipForward
|
|
||||||
v-else
|
|
||||||
class="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -800,42 +429,6 @@
|
|||||||
<Settings class="h-3 w-3 mr-1" />
|
<Settings class="h-3 w-3 mr-1" />
|
||||||
配置
|
配置
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
v-if="canRetryUpgradeNode(node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-7 px-2 text-xs"
|
|
||||||
:disabled="isUpgradeNodeActioning(node.id)"
|
|
||||||
@click="handleRetryUpgradeNode(node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(node.id)"
|
|
||||||
class="h-3 w-3 mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<RotateCcw
|
|
||||||
v-else
|
|
||||||
class="h-3 w-3 mr-1"
|
|
||||||
/>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="canSkipUpgradeNode(node)"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-7 px-2 text-xs"
|
|
||||||
:disabled="isUpgradeNodeActioning(node.id)"
|
|
||||||
@click="handleSkipUpgradeNode(node)"
|
|
||||||
>
|
|
||||||
<Loader2
|
|
||||||
v-if="isUpgradeNodeActioning(node.id)"
|
|
||||||
class="h-3 w-3 mr-1 animate-spin"
|
|
||||||
/>
|
|
||||||
<SkipForward
|
|
||||||
v-else
|
|
||||||
class="h-3 w-3 mr-1"
|
|
||||||
/>
|
|
||||||
跳过
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1069,7 +662,7 @@
|
|||||||
<Dialog
|
<Dialog
|
||||||
:model-value="showBatchUpgradeDialog"
|
:model-value="showBatchUpgradeDialog"
|
||||||
title="批量升级"
|
title="批量升级"
|
||||||
description="向所有在线 tunnel 节点下发升级版本指令"
|
description="给所有 tunnel 节点写入升级目标,节点会在下次心跳自动领取"
|
||||||
:icon="Settings"
|
:icon="Settings"
|
||||||
size="sm"
|
size="sm"
|
||||||
@update:model-value="(open: boolean) => { if (!open) { resetBatchUpgradeDialog() } }"
|
@update:model-value="(open: boolean) => { if (!open) { resetBatchUpgradeDialog() } }"
|
||||||
@@ -1084,29 +677,8 @@
|
|||||||
v-model="batchUpgradeVersion"
|
v-model="batchUpgradeVersion"
|
||||||
placeholder="例如 0.2.3"
|
placeholder="例如 0.2.3"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label>升级健康探测 URL</Label>
|
|
||||||
<Input
|
|
||||||
v-model="batchUpgradeProbeUrl"
|
|
||||||
placeholder="可选,例如 https://example.com/health"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
留空则只等待真实流量或手动确认;填写后 gateway 会通过 tunnel 主动发起 GET 探测。
|
gateway 只会写入 `upgrade_to` 目标版本,不再维护分波 rollout 或确认状态。
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label>探测超时(秒)</Label>
|
|
||||||
<Input
|
|
||||||
v-model="batchUpgradeProbeTimeout"
|
|
||||||
type="number"
|
|
||||||
min="5"
|
|
||||||
max="60"
|
|
||||||
inputmode="numeric"
|
|
||||||
placeholder="默认 10"
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
仅在填写探测 URL 时生效,允许范围 5 到 60 秒。
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -1211,7 +783,6 @@ import {
|
|||||||
type ProxyNodeRemoteConfig,
|
type ProxyNodeRemoteConfig,
|
||||||
type ProxyNodeSchedulingState,
|
type ProxyNodeSchedulingState,
|
||||||
type ProxyNodeTestResult,
|
type ProxyNodeTestResult,
|
||||||
type ProxyNodeUpgradeRolloutTrackedNode,
|
|
||||||
} from '@/api/proxy-nodes'
|
} from '@/api/proxy-nodes'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -1236,7 +807,7 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
} from '@/components/ui'
|
} from '@/components/ui'
|
||||||
|
|
||||||
import { Search, Trash2, Plus, SquarePen, Activity, Loader2, Settings, History, RotateCcw, SkipForward } from 'lucide-vue-next'
|
import { Search, Trash2, Plus, SquarePen, Activity, Loader2, Settings, History } from 'lucide-vue-next'
|
||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import { formatRegion } from '@/utils/region'
|
import { formatRegion } from '@/utils/region'
|
||||||
import HardwareTooltip from './components/HardwareTooltip.vue'
|
import HardwareTooltip from './components/HardwareTooltip.vue'
|
||||||
@@ -1247,7 +818,6 @@ const store = useProxyNodesStore()
|
|||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const filterStatus = ref('all')
|
const filterStatus = ref('all')
|
||||||
const filterRolloutStage = ref<RolloutStageFilterValue>('all')
|
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
|
|
||||||
@@ -1276,13 +846,7 @@ const configForm = ref({
|
|||||||
})
|
})
|
||||||
const showBatchUpgradeDialog = ref(false)
|
const showBatchUpgradeDialog = ref(false)
|
||||||
const batchUpgradeVersion = ref('')
|
const batchUpgradeVersion = ref('')
|
||||||
const batchUpgradeProbeUrl = ref('')
|
|
||||||
const batchUpgradeProbeTimeout = ref('10')
|
|
||||||
const batchUpgrading = ref(false)
|
const batchUpgrading = ref(false)
|
||||||
const clearingUpgradeConflicts = ref(false)
|
|
||||||
const cancellingUpgradeRollout = ref(false)
|
|
||||||
const restoringSkippedUpgradeNodes = ref(false)
|
|
||||||
const actioningUpgradeNodes = ref(new Set<string>())
|
|
||||||
|
|
||||||
// 连接事件对话框
|
// 连接事件对话框
|
||||||
const showEventsDialog = ref(false)
|
const showEventsDialog = ref(false)
|
||||||
@@ -1294,95 +858,6 @@ const loadingEvents = ref(false)
|
|||||||
const testingNodes = ref(new Set<string>())
|
const testingNodes = ref(new Set<string>())
|
||||||
const testingUrl = ref(false)
|
const testingUrl = ref(false)
|
||||||
|
|
||||||
const rolloutTrackedNodeById = computed(() => new Map(
|
|
||||||
(store.rollout?.tracked_nodes ?? []).map(node => [node.node_id, node] as const)
|
|
||||||
))
|
|
||||||
|
|
||||||
const rolloutConflictNodeIds = computed(() => new Set(store.rollout?.conflict_node_ids ?? []))
|
|
||||||
const rolloutCompletedNodeIds = computed(() => new Set(store.rollout?.completed_node_ids ?? []))
|
|
||||||
const rolloutSkippedNodeIds = computed(() => new Set(store.rollout?.skipped_node_ids ?? []))
|
|
||||||
|
|
||||||
const rolloutPendingPreview = computed(() => {
|
|
||||||
if (!store.rollout) return []
|
|
||||||
return store.rollout.pending_node_ids
|
|
||||||
.map(nodeId => {
|
|
||||||
const node = store.nodes.find(candidate => candidate.id === nodeId)
|
|
||||||
if (!node) return null
|
|
||||||
const badge = nodeRolloutBadge(node)
|
|
||||||
return {
|
|
||||||
node_id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
node,
|
|
||||||
label: badge?.label ?? '等待确认',
|
|
||||||
variant: badge?.variant ?? 'secondary',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((entry): entry is {
|
|
||||||
node_id: string
|
|
||||||
name: string
|
|
||||||
node: ProxyNode
|
|
||||||
label: string
|
|
||||||
variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'dark'
|
|
||||||
} => entry !== null)
|
|
||||||
.slice(0, 6)
|
|
||||||
})
|
|
||||||
|
|
||||||
const hiddenPendingNodeCount = computed(() =>
|
|
||||||
Math.max(0, (store.rollout?.pending_node_ids.length ?? 0) - rolloutPendingPreview.value.length)
|
|
||||||
)
|
|
||||||
|
|
||||||
const rolloutConflictPreview = computed(() => {
|
|
||||||
if (!store.rollout) return []
|
|
||||||
return store.rollout.conflict_node_ids
|
|
||||||
.map(nodeId => {
|
|
||||||
const node = store.nodes.find(candidate => candidate.id === nodeId)
|
|
||||||
if (!node) return null
|
|
||||||
return {
|
|
||||||
node_id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
node,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((entry): entry is {
|
|
||||||
node_id: string
|
|
||||||
name: string
|
|
||||||
node: ProxyNode
|
|
||||||
} => entry !== null)
|
|
||||||
.slice(0, 6)
|
|
||||||
})
|
|
||||||
|
|
||||||
const hiddenConflictNodeCount = computed(() =>
|
|
||||||
Math.max(0, (store.rollout?.conflict_node_ids.length ?? 0) - rolloutConflictPreview.value.length)
|
|
||||||
)
|
|
||||||
|
|
||||||
const rolloutSkippedPreview = computed(() => {
|
|
||||||
if (!store.rollout) return []
|
|
||||||
return store.rollout.skipped_node_ids
|
|
||||||
.map(nodeId => {
|
|
||||||
const node = store.nodes.find(candidate => candidate.id === nodeId)
|
|
||||||
if (!node) return null
|
|
||||||
return {
|
|
||||||
node_id: node.id,
|
|
||||||
name: node.name,
|
|
||||||
node,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((entry): entry is {
|
|
||||||
node_id: string
|
|
||||||
name: string
|
|
||||||
node: ProxyNode
|
|
||||||
} => entry !== null)
|
|
||||||
.slice(0, 6)
|
|
||||||
})
|
|
||||||
|
|
||||||
const hiddenSkippedNodeCount = computed(() =>
|
|
||||||
Math.max(0, (store.rollout?.skipped_node_ids.length ?? 0) - rolloutSkippedPreview.value.length)
|
|
||||||
)
|
|
||||||
|
|
||||||
const activeRolloutStageLabel = computed(() =>
|
|
||||||
rolloutStageOptions.find(option => option.value === filterRolloutStage.value)?.label ?? '全部阶段'
|
|
||||||
)
|
|
||||||
|
|
||||||
const filteredNodes = computed(() => {
|
const filteredNodes = computed(() => {
|
||||||
let filtered = [...store.nodes]
|
let filtered = [...store.nodes]
|
||||||
|
|
||||||
@@ -1398,10 +873,6 @@ const filteredNodes = computed(() => {
|
|||||||
filtered = filtered.filter(node => node.status === filterStatus.value)
|
filtered = filtered.filter(node => node.status === filterStatus.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterRolloutStage.value !== 'all') {
|
|
||||||
filtered = filtered.filter(node => nodeMatchesRolloutStage(node, filterRolloutStage.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1410,19 +881,10 @@ const paginatedNodes = computed(() => {
|
|||||||
return filteredNodes.value.slice(start, start + pageSize.value)
|
return filteredNodes.value.slice(start, start + pageSize.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([searchQuery, filterStatus, filterRolloutStage], () => {
|
watch([searchQuery, filterStatus], () => {
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
|
||||||
() => store.rollout,
|
|
||||||
rollout => {
|
|
||||||
if (!rollout) {
|
|
||||||
filterRolloutStage.value = 'all'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.fetchNodes()
|
await store.fetchNodes()
|
||||||
})
|
})
|
||||||
@@ -1605,21 +1067,11 @@ async function handleBatchUpgrade() {
|
|||||||
if (!version || batchUpgrading.value) return
|
if (!version || batchUpgrading.value) return
|
||||||
batchUpgrading.value = true
|
batchUpgrading.value = true
|
||||||
try {
|
try {
|
||||||
const probeUrl = batchUpgradeProbeUrl.value.trim()
|
const result = await proxyNodesApi.batchUpgrade(version)
|
||||||
const rawProbeTimeout = batchUpgradeProbeTimeout.value.trim()
|
if (result.updated > 0) {
|
||||||
const parsedProbeTimeout = rawProbeTimeout ? parseInt(rawProbeTimeout, 10) : NaN
|
success(`已向 ${result.updated} 个节点写入升级目标 ${result.version},${result.skipped} 个节点无需变更`)
|
||||||
const probeTimeoutSecs = probeUrl && !isNaN(parsedProbeTimeout)
|
|
||||||
? Math.min(60, Math.max(5, parsedProbeTimeout))
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const result = await proxyNodesApi.batchUpgrade(version, {
|
|
||||||
probe_url: probeUrl || undefined,
|
|
||||||
probe_timeout_secs: probeUrl ? probeTimeoutSecs : undefined,
|
|
||||||
})
|
|
||||||
if (result.blocked) {
|
|
||||||
success(`升级 rollout 正在等待 ${result.pending_node_ids.length} 个节点确认到 ${result.version}`)
|
|
||||||
} else {
|
} else {
|
||||||
success(`升级指令已下发:本波 ${result.updated} 个节点,跳过 ${result.skipped} 个,已完成 ${result.completed} 个,剩余 ${result.remaining} 个`)
|
success(`当前没有需要变更的 tunnel 节点,目标版本仍为 ${result.version}`)
|
||||||
}
|
}
|
||||||
resetBatchUpgradeDialog()
|
resetBatchUpgradeDialog()
|
||||||
await store.fetchNodes()
|
await store.fetchNodes()
|
||||||
@@ -1630,129 +1082,9 @@ async function handleBatchUpgrade() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCancelUpgradeRollout() {
|
|
||||||
if (!store.rollout || cancellingUpgradeRollout.value) return
|
|
||||||
const confirmed = await confirmDanger(
|
|
||||||
`确定要取消当前升级 rollout 吗?这会停止后续自动放量,但不会回滚已经下发到节点的升级指令。`,
|
|
||||||
'取消升级 Rollout',
|
|
||||||
'取消 Rollout'
|
|
||||||
)
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
cancellingUpgradeRollout.value = true
|
|
||||||
try {
|
|
||||||
const result = await proxyNodesApi.cancelUpgradeRollout()
|
|
||||||
await store.fetchNodes()
|
|
||||||
if (result.cancelled) {
|
|
||||||
success(
|
|
||||||
`已取消 ${result.version || '当前'} rollout,待确认节点 ${(result.pending_node_ids?.length ?? 0)} 个,冲突节点 ${(result.conflict_node_ids?.length ?? 0)} 个`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
success('当前没有活动中的 rollout')
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toastError(parseApiError(err, '取消 rollout 失败'))
|
|
||||||
} finally {
|
|
||||||
cancellingUpgradeRollout.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClearUpgradeConflicts() {
|
|
||||||
if (!store.rollout || clearingUpgradeConflicts.value || store.rollout.conflict_node_ids.length === 0) return
|
|
||||||
const confirmed = await confirmWarning(
|
|
||||||
`确定要清空 ${store.rollout.conflict_node_ids.length} 个冲突节点上的升级目标吗?这不会回滚已经下发成功的节点,但会解除当前 rollout 的冲突阻塞。`,
|
|
||||||
'清空冲突升级目标'
|
|
||||||
)
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
clearingUpgradeConflicts.value = true
|
|
||||||
try {
|
|
||||||
const result = await proxyNodesApi.clearUpgradeConflicts()
|
|
||||||
await store.fetchNodes()
|
|
||||||
if (result.cleared > 0) {
|
|
||||||
success(
|
|
||||||
`已清空 ${result.cleared} 个冲突节点;本次新增下发 ${result.updated} 个节点,剩余 ${result.remaining} 个`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
success('当前没有需要清理的冲突节点')
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toastError(parseApiError(err, '清空冲突失败'))
|
|
||||||
} finally {
|
|
||||||
clearingUpgradeConflicts.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRestoreSkippedUpgradeNodes() {
|
|
||||||
if (!store.rollout || restoringSkippedUpgradeNodes.value || store.rollout.skipped_node_ids.length === 0) return
|
|
||||||
const confirmed = await confirmWarning(
|
|
||||||
`确定要恢复 ${store.rollout.skipped_node_ids.length} 个已跳过节点吗?它们会重新进入当前 rollout 队列;如果当前没有待确认节点,gateway 会按批次重新下发升级。`,
|
|
||||||
'恢复已跳过节点'
|
|
||||||
)
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
restoringSkippedUpgradeNodes.value = true
|
|
||||||
try {
|
|
||||||
const result = await proxyNodesApi.restoreSkippedUpgradeNodes()
|
|
||||||
await store.fetchNodes()
|
|
||||||
if (result.restored > 0) {
|
|
||||||
success(
|
|
||||||
`已恢复 ${result.restored} 个已跳过节点;本次新增下发 ${result.updated} 个节点,剩余 ${result.remaining} 个`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
success('当前没有需要恢复的已跳过节点')
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toastError(parseApiError(err, '恢复已跳过节点失败'))
|
|
||||||
} finally {
|
|
||||||
restoringSkippedUpgradeNodes.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSkipUpgradeNode(node: ProxyNode) {
|
|
||||||
if (!canSkipUpgradeNode(node) || isUpgradeNodeActioning(node.id)) return
|
|
||||||
const confirmed = await confirmWarning(
|
|
||||||
`确定要在当前 rollout 中跳过节点 "${node.name}" 吗?这会把它排除出本轮升级,并清除当前 rollout 下发给它的升级目标。`,
|
|
||||||
'跳过当前节点'
|
|
||||||
)
|
|
||||||
if (!confirmed) return
|
|
||||||
|
|
||||||
setUpgradeNodeActioning(node.id, true)
|
|
||||||
try {
|
|
||||||
const result = await proxyNodesApi.skipUpgradeNode(node.id)
|
|
||||||
await store.fetchNodes()
|
|
||||||
success(
|
|
||||||
`已跳过节点 ${node.name};本次新增下发 ${result.updated} 个节点,剩余 ${result.remaining} 个`
|
|
||||||
)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toastError(parseApiError(err, '跳过节点失败'))
|
|
||||||
} finally {
|
|
||||||
setUpgradeNodeActioning(node.id, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRetryUpgradeNode(node: ProxyNode) {
|
|
||||||
if (!canRetryUpgradeNode(node) || isUpgradeNodeActioning(node.id)) return
|
|
||||||
|
|
||||||
setUpgradeNodeActioning(node.id, true)
|
|
||||||
try {
|
|
||||||
const result = await proxyNodesApi.retryUpgradeNode(node.id)
|
|
||||||
await store.fetchNodes()
|
|
||||||
success(
|
|
||||||
`已重试节点 ${node.name};当前待确认 ${result.pending_node_ids.length} 个,剩余 ${result.remaining} 个`
|
|
||||||
)
|
|
||||||
} catch (err: unknown) {
|
|
||||||
toastError(parseApiError(err, '重试节点失败'))
|
|
||||||
} finally {
|
|
||||||
setUpgradeNodeActioning(node.id, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetBatchUpgradeDialog() {
|
function resetBatchUpgradeDialog() {
|
||||||
showBatchUpgradeDialog.value = false
|
showBatchUpgradeDialog.value = false
|
||||||
batchUpgradeVersion.value = ''
|
batchUpgradeVersion.value = ''
|
||||||
batchUpgradeProbeUrl.value = ''
|
|
||||||
batchUpgradeProbeTimeout.value = '10'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(node: ProxyNode) {
|
async function handleDelete(node: ProxyNode) {
|
||||||
@@ -1911,93 +1243,23 @@ function nodeProxyVersion(node: ProxyNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'dark'
|
type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'dark'
|
||||||
type RolloutStageFilterValue =
|
|
||||||
| 'all'
|
|
||||||
| 'pending'
|
|
||||||
| 'conflict'
|
|
||||||
| 'skipped'
|
|
||||||
| 'completed'
|
|
||||||
| 'awaiting_version'
|
|
||||||
| 'awaiting_traffic'
|
|
||||||
| 'cooling_down'
|
|
||||||
| 'unhealthy'
|
|
||||||
| 'ready_to_finalize'
|
|
||||||
|
|
||||||
const rolloutStageOptions: Array<{ value: RolloutStageFilterValue; label: string }> = [
|
|
||||||
{ value: 'all', label: '全部阶段' },
|
|
||||||
{ value: 'pending', label: '全部卡点' },
|
|
||||||
{ value: 'awaiting_version', label: '等待版本' },
|
|
||||||
{ value: 'awaiting_traffic', label: '等待流量' },
|
|
||||||
{ value: 'cooling_down', label: '冷却中' },
|
|
||||||
{ value: 'unhealthy', label: '健康失败' },
|
|
||||||
{ value: 'ready_to_finalize', label: '等待收尾' },
|
|
||||||
{ value: 'conflict', label: '版本冲突' },
|
|
||||||
{ value: 'skipped', label: '已跳过' },
|
|
||||||
{ value: 'completed', label: '已完成' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function normalizeVersion(value: string | null | undefined) {
|
function normalizeVersion(value: string | null | undefined) {
|
||||||
const trimmed = (value || '').trim().toLowerCase()
|
const trimmed = (value || '').trim().toLowerCase()
|
||||||
return trimmed.startsWith('proxy-v') ? trimmed.slice('proxy-v'.length) : trimmed
|
return trimmed.startsWith('proxy-v') ? trimmed.slice('proxy-v'.length) : trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDurationShort(seconds: number | null | undefined) {
|
|
||||||
if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) return '0s'
|
|
||||||
if (seconds < 60) return `${Math.floor(seconds)}s`
|
|
||||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
|
|
||||||
return `${Math.floor(seconds / 3600)}h`
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTrackedNodeBadge(
|
|
||||||
tracked: ProxyNodeUpgradeRolloutTrackedNode,
|
|
||||||
): { label: string; variant: BadgeVariant } {
|
|
||||||
switch (tracked.state) {
|
|
||||||
case 'awaiting_version':
|
|
||||||
return { label: '等待版本上报', variant: 'secondary' }
|
|
||||||
case 'awaiting_traffic':
|
|
||||||
return { label: '等待流量确认', variant: 'warning' }
|
|
||||||
case 'cooling_down':
|
|
||||||
return {
|
|
||||||
label: tracked.cooldown_remaining_secs && tracked.cooldown_remaining_secs > 0
|
|
||||||
? `冷却中 ${formatDurationShort(tracked.cooldown_remaining_secs)}`
|
|
||||||
: '冷却中',
|
|
||||||
variant: 'warning',
|
|
||||||
}
|
|
||||||
case 'unhealthy':
|
|
||||||
return { label: '健康检查未过', variant: 'destructive' }
|
|
||||||
case 'ready_to_finalize':
|
|
||||||
return { label: '等待收尾', variant: 'success' }
|
|
||||||
default:
|
|
||||||
return { label: '升级中', variant: 'secondary' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeRolloutBadge(node: ProxyNode): { label: string; variant: BadgeVariant } | null {
|
function nodeRolloutBadge(node: ProxyNode): { label: string; variant: BadgeVariant } | null {
|
||||||
const rollout = store.rollout
|
if (node.is_manual || !node.tunnel_mode) return null
|
||||||
if (!rollout || node.is_manual || !node.tunnel_mode) return null
|
const rawTarget = node.remote_config?.upgrade_to?.trim()
|
||||||
if (rolloutSkippedNodeIds.value.has(node.id)) {
|
if (!rawTarget) return null
|
||||||
return { label: '已跳过', variant: 'outline' }
|
|
||||||
|
const targetVersion = normalizeVersion(rawTarget)
|
||||||
|
const currentVersion = normalizeVersion(nodeProxyVersion(node))
|
||||||
|
if (targetVersion && currentVersion && targetVersion === currentVersion) {
|
||||||
|
return { label: `目标版本 ${rawTarget}`, variant: 'success' }
|
||||||
}
|
}
|
||||||
if (rolloutConflictNodeIds.value.has(node.id)) {
|
return { label: `待升级 ${rawTarget}`, variant: 'warning' }
|
||||||
return { label: '版本冲突', variant: 'destructive' }
|
|
||||||
}
|
|
||||||
const tracked = rolloutTrackedNodeById.value.get(node.id)
|
|
||||||
if (tracked) {
|
|
||||||
return resolveTrackedNodeBadge(tracked)
|
|
||||||
}
|
|
||||||
if (rolloutCompletedNodeIds.value.has(node.id)) {
|
|
||||||
const nodeVersion = normalizeVersion(nodeProxyVersion(node))
|
|
||||||
if (nodeVersion && nodeVersion === normalizeVersion(rollout.version)) {
|
|
||||||
return { label: '已确认完成', variant: 'success' }
|
|
||||||
}
|
|
||||||
return { label: '目标版本', variant: 'outline' }
|
|
||||||
}
|
|
||||||
const targetVersion = normalizeVersion(rollout.version)
|
|
||||||
const remoteUpgradeTarget = normalizeVersion(node.remote_config?.upgrade_to ?? null)
|
|
||||||
if (remoteUpgradeTarget && remoteUpgradeTarget === targetVersion) {
|
|
||||||
return { label: '已下发升级', variant: 'secondary' }
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeSchedulingBadge(node: ProxyNode): { label: string; variant: BadgeVariant } | null {
|
function nodeSchedulingBadge(node: ProxyNode): { label: string; variant: BadgeVariant } | null {
|
||||||
@@ -2010,80 +1272,4 @@ function nodeSchedulingBadge(node: ProxyNode): { label: string; variant: BadgeVa
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function rolloutStageOfNode(node: ProxyNode): Exclude<RolloutStageFilterValue, 'all'> | null {
|
|
||||||
const rollout = store.rollout
|
|
||||||
if (!rollout || node.is_manual || !node.tunnel_mode) return null
|
|
||||||
if (rolloutSkippedNodeIds.value.has(node.id)) {
|
|
||||||
return 'skipped'
|
|
||||||
}
|
|
||||||
if (rolloutConflictNodeIds.value.has(node.id)) {
|
|
||||||
return 'conflict'
|
|
||||||
}
|
|
||||||
const tracked = rolloutTrackedNodeById.value.get(node.id)
|
|
||||||
if (tracked) {
|
|
||||||
return tracked.state
|
|
||||||
}
|
|
||||||
if (rolloutCompletedNodeIds.value.has(node.id)) {
|
|
||||||
return 'completed'
|
|
||||||
}
|
|
||||||
if (rollout.pending_node_ids.includes(node.id)) {
|
|
||||||
return 'pending'
|
|
||||||
}
|
|
||||||
const targetVersion = normalizeVersion(rollout.version)
|
|
||||||
const remoteUpgradeTarget = normalizeVersion(node.remote_config?.upgrade_to ?? null)
|
|
||||||
if (remoteUpgradeTarget && remoteUpgradeTarget === targetVersion) {
|
|
||||||
return 'pending'
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeMatchesRolloutStage(node: ProxyNode, stage: RolloutStageFilterValue) {
|
|
||||||
if (stage === 'all') return true
|
|
||||||
const resolvedStage = rolloutStageOfNode(node)
|
|
||||||
if (!resolvedStage) return false
|
|
||||||
if (stage === 'pending') {
|
|
||||||
return resolvedStage === 'pending'
|
|
||||||
|| resolvedStage === 'awaiting_version'
|
|
||||||
|| resolvedStage === 'awaiting_traffic'
|
|
||||||
|| resolvedStage === 'cooling_down'
|
|
||||||
|| resolvedStage === 'unhealthy'
|
|
||||||
|| resolvedStage === 'ready_to_finalize'
|
|
||||||
}
|
|
||||||
return resolvedStage === stage
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUpgradeNodeActioning(nodeId: string) {
|
|
||||||
return actioningUpgradeNodes.value.has(nodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUpgradeNodeActioning(nodeId: string, active: boolean) {
|
|
||||||
const next = new Set(actioningUpgradeNodes.value)
|
|
||||||
if (active) {
|
|
||||||
next.add(nodeId)
|
|
||||||
} else {
|
|
||||||
next.delete(nodeId)
|
|
||||||
}
|
|
||||||
actioningUpgradeNodes.value = next
|
|
||||||
}
|
|
||||||
|
|
||||||
function canSkipUpgradeNode(node: ProxyNode) {
|
|
||||||
const rollout = store.rollout
|
|
||||||
if (!rollout || node.is_manual || !node.tunnel_mode) return false
|
|
||||||
if (rolloutSkippedNodeIds.value.has(node.id) || rolloutCompletedNodeIds.value.has(node.id)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return rolloutConflictNodeIds.value.has(node.id)
|
|
||||||
|| rolloutTrackedNodeById.value.has(node.id)
|
|
||||||
|| rollout.pending_node_ids.includes(node.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function canRetryUpgradeNode(node: ProxyNode) {
|
|
||||||
const rollout = store.rollout
|
|
||||||
if (!rollout || node.is_manual || !node.tunnel_mode) return false
|
|
||||||
if (rolloutCompletedNodeIds.value.has(node.id)) return false
|
|
||||||
return rolloutSkippedNodeIds.value.has(node.id)
|
|
||||||
|| rolloutConflictNodeIds.value.has(node.id)
|
|
||||||
|| rolloutTrackedNodeById.value.has(node.id)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user