fix(ui): keep device details open across identity changes

This commit is contained in:
lda committed 2026-10-12 23:59:23 +07:00 Verified
1 parent 4eef8c758f
commit 4e290e6de8
12 files changed
+324 -32

No files matched your search

+1 -1
View File
@@ -14,5 +14,5 @@ pub use enroll::{
};
pub(crate) use fleet::FleetCache;
pub(crate) use fleet::sample_fleet_counts;
pub use fleet::{list_fleet_devices, refresh_fleet_devices, wake_fleet_device};
pub use fleet::{get_fleet_device, list_fleet_devices, refresh_fleet_devices, wake_fleet_device};
pub use stats::{StateStatsResponse, state_stats};
@@ -1,7 +1,7 @@
use std::collections::{BTreeSet, HashMap};
use axum::Json;
use axum::extract::{Query, State};
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use tracing::warn;
@@ -105,6 +105,30 @@ pub async fn list_fleet_devices(
}
}
/// Details are an exact identity lookup, not a filtered/paginated list lookup.
/// In particular, remembering an unsaved row must remain readable under its new key.
#[tracing::instrument(name = "fleet_get", skip_all)]
pub async fn get_fleet_device(
State(state): State<AppState>,
Path(device_key): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
let devices = fleet_view(&state)
.await
.map_err(|err| fleet_error(err, "get_fleet_device_failed"))?;
let device = devices
.iter()
.find(|device| device.device_key == device_key)
.cloned()
.ok_or_else(|| {
ApiError::new(
StatusCode::NOT_FOUND,
"fleet_device_not_found",
"fleet device not found",
)
})?;
Ok(Json(device))
}
#[tracing::instrument(name = "fleet_load", skip_all, fields(grouped = tracing::field::Empty, filtered = tracing::field::Empty, returned = tracing::field::Empty))]
async fn load_fleet_devices(
state: &AppState,
@@ -159,6 +159,35 @@ async fn fleet_cache_tracks_store_edits_and_connections_without_caching_filters(
.unwrap()
.is_empty()
);
// The filtered list excludes this row, but exact details still follow the
// authoritative saved key after remembering and renaming the observation.
{
use axum::{
extract::{Path, State},
response::IntoResponse,
};
let response = super::get_fleet_device(
State(state.clone()),
Path(format!("known:{}", saved.device_id)),
)
.await
.unwrap()
.into_response();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let details: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(details["display_name"], "renamed pc");
assert_eq!(details["known_device"]["device_id"], saved.device_id);
let missing =
super::get_fleet_device(State(state.clone()), Path(initial[0].device_key.clone()))
.await
.err()
.expect("old unsaved grouping key no longer exists");
assert_eq!(missing.status, axum::http::StatusCode::NOT_FOUND);
assert_eq!(missing.code, "fleet_device_not_found");
}
let counts = super::sample_fleet_counts(&state).await.unwrap();
assert_eq!((counts.saved, counts.unsaved), (1, 0));
let all = ListFleetDevicesQuery {
@@ -216,6 +216,20 @@ mod tests {
.unwrap();
assert_eq!(listed.len(), 1000);
assert!(listed.iter().all(|row| row.device_key != "pc-1000"));
{
use axum::extract::Path;
let details =
super::super::get_fleet_device(State(state.clone()), Path("pc-1000".into()))
.await
.unwrap()
.into_response();
assert_eq!(details.status(), StatusCode::OK);
let body = axum::body::to_bytes(details.into_body(), usize::MAX)
.await
.unwrap();
let row: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(row["device_key"], "pc-1000");
}
for route_id in [None, Some("valid")] {
let selected = load_selected_route(&state, "pc-1000", route_id)
.await
+4 -4
View File
@@ -15,10 +15,10 @@ pub use commands::{list_agents, run_command};
pub use control::{
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeAgentResponse, RevokeEnrollTokenResponse,
StateStatsResponse, attach_device_identifier, create_known_device, detach_device_identifier,
enroll, forget_known_device, get_known_device, healthz, issue_enroll_token, list_enroll_tokens,
list_fleet_devices, list_known_devices, merge_known_device, refresh_fleet_devices,
rename_known_device, revoke_agent, revoke_enroll_token, set_agent_nickname, state_stats,
wake_fleet_device,
enroll, forget_known_device, get_fleet_device, get_known_device, healthz, issue_enroll_token,
list_enroll_tokens, list_fleet_devices, list_known_devices, merge_known_device,
refresh_fleet_devices, rename_known_device, revoke_agent, revoke_enroll_token,
set_agent_nickname, state_stats, wake_fleet_device,
};
pub use terminals::{
agent_terminal_ws, attach_terminal, close_terminal, create_terminal, get_terminal,
+4
View File
@@ -109,6 +109,10 @@ fn control_api_routes() -> Router<AppState> {
"/api/v1/control/fleet/devices/refresh",
post(api::refresh_fleet_devices),
)
.route(
"/api/v1/control/fleet/devices/{device_key}",
get(api::get_fleet_device),
)
.route("/api/v1/control/fleet/wake", post(api::wake_fleet_device))
.route(
"/api/v1/control/devices",