mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 12:40:31 +00:00
Add dashboard metrics, activity log, and dark theme
- Add DashboardMetrics for tracking downloads/uploads/cache hits per registry - Add ActivityLog for recent activity with bounded size (50 entries) - Instrument Docker, npm, Maven, and Cargo handlers with metrics - Add /api/ui/dashboard endpoint with global stats and activity - Implement dark theme dashboard with real-time polling (5s interval) - Add mount points table showing registry paths and proxy upstreams
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::activity_log::{ActionType, ActivityEntry};
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
@@ -37,7 +38,17 @@ async fn download(
|
||||
crate_name, version, crate_name, version
|
||||
);
|
||||
match state.storage.get(&key).await {
|
||||
Ok(data) => (StatusCode::OK, data).into_response(),
|
||||
Ok(data) => {
|
||||
state.metrics.record_download("cargo");
|
||||
state.metrics.record_cache_hit();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
format!("{}@{}", crate_name, version),
|
||||
"cargo",
|
||||
"LOCAL",
|
||||
));
|
||||
(StatusCode::OK, data).into_response()
|
||||
}
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::activity_log::{ActionType, ActivityEntry};
|
||||
use crate::validation::{validate_digest, validate_docker_name, validate_docker_reference};
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
@@ -75,12 +76,22 @@ async fn download_blob(
|
||||
|
||||
let key = format!("docker/{}/blobs/{}", name, digest);
|
||||
match state.storage.get(&key).await {
|
||||
Ok(data) => (
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||
data,
|
||||
)
|
||||
.into_response(),
|
||||
Ok(data) => {
|
||||
state.metrics.record_download("docker");
|
||||
state.metrics.record_cache_hit();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
format!("{}@{}", name, &digest[..19.min(digest.len())]),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
));
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/octet-stream")],
|
||||
data,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
@@ -176,6 +187,13 @@ async fn upload_blob(
|
||||
let key = format!("docker/{}/blobs/{}", name, digest);
|
||||
match state.storage.put(&key, &data).await {
|
||||
Ok(()) => {
|
||||
state.metrics.record_upload("docker");
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::Push,
|
||||
format!("{}@{}", name, &digest[..19.min(digest.len())]),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
));
|
||||
let location = format!("/v2/{}/blobs/{}", name, digest);
|
||||
(StatusCode::CREATED, [(header::LOCATION, location)]).into_response()
|
||||
}
|
||||
@@ -197,6 +215,15 @@ async fn get_manifest(
|
||||
let key = format!("docker/{}/manifests/{}.json", name, reference);
|
||||
match state.storage.get(&key).await {
|
||||
Ok(data) => {
|
||||
state.metrics.record_download("docker");
|
||||
state.metrics.record_cache_hit();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
format!("{}:{}", name, reference),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
));
|
||||
|
||||
// Calculate digest for Docker-Content-Digest header
|
||||
use sha2::Digest;
|
||||
let digest = format!("sha256:{:x}", sha2::Sha256::digest(&data));
|
||||
@@ -245,6 +272,14 @@ async fn put_manifest(
|
||||
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
|
||||
}
|
||||
|
||||
state.metrics.record_upload("docker");
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::Push,
|
||||
format!("{}:{}", name, reference),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
));
|
||||
|
||||
let location = format!("/v2/{}/manifests/{}", name, reference);
|
||||
(
|
||||
StatusCode::CREATED,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::activity_log::{ActionType, ActivityEntry};
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
@@ -19,8 +20,19 @@ pub fn routes() -> Router<Arc<AppState>> {
|
||||
async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>) -> Response {
|
||||
let key = format!("maven/{}", path);
|
||||
|
||||
// Extract artifact name for logging (last 2-3 path components)
|
||||
let artifact_name = path.split('/').rev().take(3).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("/");
|
||||
|
||||
// Try local storage first
|
||||
if let Ok(data) = state.storage.get(&key).await {
|
||||
state.metrics.record_download("maven");
|
||||
state.metrics.record_cache_hit();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::CacheHit,
|
||||
artifact_name,
|
||||
"maven",
|
||||
"CACHE",
|
||||
));
|
||||
return with_content_type(&path, data).into_response();
|
||||
}
|
||||
|
||||
@@ -30,6 +42,15 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
|
||||
|
||||
match fetch_from_proxy(&url, state.config.maven.proxy_timeout).await {
|
||||
Ok(data) => {
|
||||
state.metrics.record_download("maven");
|
||||
state.metrics.record_cache_miss();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::ProxyFetch,
|
||||
artifact_name,
|
||||
"maven",
|
||||
"PROXY",
|
||||
));
|
||||
|
||||
// Cache in local storage (fire and forget)
|
||||
let storage = state.storage.clone();
|
||||
let key_clone = key.clone();
|
||||
@@ -53,8 +74,21 @@ async fn upload(
|
||||
body: Bytes,
|
||||
) -> StatusCode {
|
||||
let key = format!("maven/{}", path);
|
||||
|
||||
// Extract artifact name for logging
|
||||
let artifact_name = path.split('/').rev().take(3).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("/");
|
||||
|
||||
match state.storage.put(&key, &body).await {
|
||||
Ok(()) => StatusCode::CREATED,
|
||||
Ok(()) => {
|
||||
state.metrics.record_upload("maven");
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::Push,
|
||||
artifact_name,
|
||||
"maven",
|
||||
"LOCAL",
|
||||
));
|
||||
StatusCode::CREATED
|
||||
}
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::activity_log::{ActionType, ActivityEntry};
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
@@ -29,8 +30,25 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
||||
format!("npm/{}/metadata.json", path)
|
||||
};
|
||||
|
||||
// Extract package name for logging
|
||||
let package_name = if is_tarball {
|
||||
path.split("/-/").next().unwrap_or(&path).to_string()
|
||||
} else {
|
||||
path.clone()
|
||||
};
|
||||
|
||||
// Try local storage first
|
||||
if let Ok(data) = state.storage.get(&key).await {
|
||||
if is_tarball {
|
||||
state.metrics.record_download("npm");
|
||||
state.metrics.record_cache_hit();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::CacheHit,
|
||||
package_name,
|
||||
"npm",
|
||||
"CACHE",
|
||||
));
|
||||
}
|
||||
return with_content_type(is_tarball, data).into_response();
|
||||
}
|
||||
|
||||
@@ -45,6 +63,17 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
|
||||
};
|
||||
|
||||
if let Ok(data) = fetch_from_proxy(&url, state.config.npm.proxy_timeout).await {
|
||||
if is_tarball {
|
||||
state.metrics.record_download("npm");
|
||||
state.metrics.record_cache_miss();
|
||||
state.activity.push(ActivityEntry::new(
|
||||
ActionType::ProxyFetch,
|
||||
package_name,
|
||||
"npm",
|
||||
"PROXY",
|
||||
));
|
||||
}
|
||||
|
||||
// Cache in local storage (fire and forget)
|
||||
let storage = state.storage.clone();
|
||||
let key_clone = key.clone();
|
||||
|
||||
Reference in New Issue
Block a user