fix(overview): locked reason, one-line comments, stricter query parsing

This commit is contained in:
Bhargav
2026-11-04 15:44:46 +05:30
parent 562de6b791
commit ff55cedb29
2 changed files with 41 additions and 24 deletions
@@ -401,7 +401,7 @@ pub async fn org_summary(Path(org_id): Path<String>) -> impl IntoResponse {
params(
("org_id" = String, Path, description = "Organization name"),
("top" = Option<u32>, Query, description = "Top streams per stream type by size. Default 5, max 20."),
("refresh" = Option<bool>, Query, description = "Set true to skip the short-lived cache and rebuild now."),
("refresh" = Option<bool>, Query, description = "Set true (or 1) to skip the short-lived cache and rebuild now."),
),
responses(
(status = 200, description = "Success", content_type = "application/json", body = Object),
@@ -418,17 +418,19 @@ pub async fn org_overview(
Headers(user_email): Headers<UserEmail>,
Query(query): Query<HashMap<String, String>>,
) -> Response {
let top = match query.get("top").map(|v| v.parse::<u32>()) {
let top = match query.get("top").map(|v| v.parse::<usize>()) {
Some(Err(_)) => {
return MetaHttpResponse::bad_request("'top' must be an integer, clamped to 1..20");
return MetaHttpResponse::bad_request(
"'top' must be a non-negative integer; it is clamped to 1..20",
);
}
Some(Ok(n)) => Some(n as usize),
Some(Ok(n)) => Some(n),
None => None,
};
let top_n = overview::normalize_top_n(top);
let refresh = query
.get("refresh")
.is_some_and(|v| v == "true" || v == "1");
.is_some_and(|v| v.eq_ignore_ascii_case("true") || v == "1");
if !refresh && let Some(hit) = overview::cached_overview(&org_id, &user_email.user_id, top_n) {
return Json(hit).into_response();
}
+34 -19
View File
@@ -39,8 +39,7 @@ use stream::try_get_streams;
const DEFAULT_TOP_N: usize = 5;
const MAX_TOP_N: usize = 20;
/// User-facing stream types only; profiles, service graph, filelist and internal streams are left
/// out.
/// User-facing stream types; derived and internal streams are left out.
const OVERVIEW_STREAM_TYPES: [StreamType; 4] = [
StreamType::Logs,
StreamType::Metrics,
@@ -97,6 +96,9 @@ pub struct Section<T> {
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// Why a section is locked, so the UI can pick the right message.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl<T> Section<T> {
@@ -105,6 +107,7 @@ impl<T> Section<T> {
status: SectionStatus::Ok,
data: Some(data),
error: None,
reason: None,
}
}
@@ -113,6 +116,7 @@ impl<T> Section<T> {
status: SectionStatus::Error,
data: None,
error: Some(message.to_string()),
reason: None,
}
}
@@ -121,14 +125,16 @@ impl<T> Section<T> {
status: SectionStatus::Forbidden,
data: None,
error: None,
reason: None,
}
}
fn locked() -> Self {
fn locked(reason: &str) -> Self {
Self {
status: SectionStatus::Locked,
data: None,
error: None,
reason: Some(reason.to_string()),
}
}
@@ -196,6 +202,7 @@ pub struct AlertsData {
pub by_state: BTreeMap<String, u64>,
/// Enabled rules by the severity level of their last classification.
pub by_level: BTreeMap<String, u64>,
/// Same count as `by_state.error`; do not add the two together.
pub failed_evaluation: u64,
}
@@ -242,14 +249,6 @@ pub async fn resolve_scope(org_id: &str, user_id: &str) -> Result<OverviewScope,
})
}
async fn list_grants(
org_id: &str,
user_id: &str,
resource: &str,
) -> Result<Option<Vec<String>>, anyhow::Error> {
crate::authz::list_objects_for_user(org_id, user_id, "GET", &ofga_object_type(resource)).await
}
/// Overview for a caller whose grants could not be resolved; each scoped section reports an error.
pub async fn scope_unavailable() -> Overview {
Overview {
@@ -297,6 +296,14 @@ pub async fn get_overview(org_id: &str, scope: &OverviewScope, top_n: usize) ->
overview
}
async fn list_grants(
org_id: &str,
user_id: &str,
resource: &str,
) -> Result<Option<Vec<String>>, anyhow::Error> {
crate::authz::list_objects_for_user(org_id, user_id, "GET", &ofga_object_type(resource)).await
}
async fn build_overview(org_id: &str, scope: &OverviewScope, top_n: usize) -> Overview {
let (streams, alerts, pipelines) = tokio::join!(
async {
@@ -494,7 +501,7 @@ async fn limits_section() -> Section<LimitsData> {
};
if get_license().await.is_none() {
return Section::locked();
return Section::locked("no license loaded");
}
Section::ok(LimitsData {
scope: "deployment",
@@ -506,7 +513,7 @@ async fn limits_section() -> Section<LimitsData> {
#[cfg(not(feature = "enterprise"))]
async fn limits_section() -> Section<LimitsData> {
Section::locked()
Section::locked("not available in this edition")
}
#[cfg(test)]
@@ -634,8 +641,11 @@ mod tests {
#[test]
fn test_section_serialization_carries_status_and_omits_empty_fields() {
let locked = serde_json::to_value(Section::<PipelinesData>::locked()).unwrap();
assert_eq!(locked, serde_json::json!({"status": "locked"}));
let locked = serde_json::to_value(Section::<PipelinesData>::locked("no license")).unwrap();
assert_eq!(
locked,
serde_json::json!({"status": "locked", "reason": "no license"})
);
let err = serde_json::to_value(Section::<PipelinesData>::error("boom")).unwrap();
assert_eq!(err, serde_json::json!({"status": "error", "error": "boom"}));
}
@@ -643,7 +653,12 @@ mod tests {
#[cfg(not(feature = "enterprise"))]
#[tokio::test]
async fn test_limits_locked_without_enterprise() {
assert_eq!(limits_section().await.status, SectionStatus::Locked);
let limits = limits_section().await;
assert_eq!(limits.status, SectionStatus::Locked);
assert_eq!(
limits.reason.as_deref(),
Some("not available in this edition")
);
}
#[tokio::test]
@@ -756,9 +771,9 @@ mod tests {
vec![stream("only_for_admin", StreamType::Logs, 7.0, 1)],
5,
)),
alerts: Section::locked(),
pipelines: Section::locked(),
limits: Section::locked(),
alerts: Section::locked("test"),
pipelines: Section::locked("test"),
limits: Section::locked("test"),
generated_at: now_micros(),
cached: false,
};