mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 13:50:31 +00:00
Fix formatting
This commit is contained in:
152
SESSION_NOTES.md
Normal file
152
SESSION_NOTES.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# NORA Development Session Notes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-01-26 - Dashboard Expansion
|
||||||
|
|
||||||
|
### Iteration 1: Planning & Exploration
|
||||||
|
- Received detailed implementation plan for dashboard expansion
|
||||||
|
- Explored codebase structure using Task agent
|
||||||
|
- Identified key files to modify:
|
||||||
|
- `main.rs` - AppState
|
||||||
|
- `ui/api.rs`, `ui/mod.rs`, `ui/components.rs`, `ui/templates.rs`
|
||||||
|
- `registry/docker.rs`, `npm.rs`, `maven.rs`, `cargo_registry.rs`
|
||||||
|
|
||||||
|
### Iteration 2: Infrastructure (Phase 1)
|
||||||
|
- Created `src/dashboard_metrics.rs`:
|
||||||
|
- `DashboardMetrics` struct with AtomicU64 counters
|
||||||
|
- Per-registry tracking (docker, npm, maven, cargo, pypi)
|
||||||
|
- `record_download()`, `record_upload()`, `record_cache_hit/miss()`
|
||||||
|
- `cache_hit_rate()` calculation
|
||||||
|
|
||||||
|
- Created `src/activity_log.rs`:
|
||||||
|
- `ActionType` enum: Pull, Push, CacheHit, ProxyFetch
|
||||||
|
- `ActivityEntry` struct with timestamp, action, artifact, registry, source
|
||||||
|
- `ActivityLog` with RwLock<VecDeque> (bounded to 50 entries)
|
||||||
|
|
||||||
|
### Iteration 3: AppState Update (Phase 2)
|
||||||
|
- Updated `main.rs`:
|
||||||
|
- Added `mod activity_log` and `mod dashboard_metrics`
|
||||||
|
- Extended `AppState` with `metrics: DashboardMetrics` and `activity: ActivityLog`
|
||||||
|
- Initialized in `run_server()`
|
||||||
|
|
||||||
|
### Iteration 4: API Endpoint (Phase 3)
|
||||||
|
- Updated `ui/api.rs`:
|
||||||
|
- Added structs: `DashboardResponse`, `GlobalStats`, `RegistryCardStats`, `MountPoint`
|
||||||
|
- Implemented `api_dashboard()` - aggregates all metrics, storage stats, activity
|
||||||
|
|
||||||
|
- Updated `ui/mod.rs`:
|
||||||
|
- Added route `/api/ui/dashboard`
|
||||||
|
- Modified `dashboard()` handler to use new response
|
||||||
|
|
||||||
|
### Iteration 5: Dark Theme UI (Phase 4)
|
||||||
|
- Updated `ui/components.rs` with ~400 new lines:
|
||||||
|
- `layout_dark()` - dark theme wrapper (#0f172a background)
|
||||||
|
- `sidebar_dark()`, `header_dark()` - dark theme navigation
|
||||||
|
- `render_global_stats()` - 5-column stats grid
|
||||||
|
- `render_registry_card()` - extended card with metrics
|
||||||
|
- `render_mount_points_table()` - registry paths and proxies
|
||||||
|
- `render_activity_row()`, `render_activity_log()` - activity display
|
||||||
|
- `render_polling_script()` - 5-second auto-refresh JS
|
||||||
|
|
||||||
|
### Iteration 6: Dashboard Template (Phase 5)
|
||||||
|
- Updated `ui/templates.rs`:
|
||||||
|
- Refactored `render_dashboard()` to accept `DashboardResponse`
|
||||||
|
- Added uptime display, global stats, registry cards grid
|
||||||
|
- Added mount points table and activity log
|
||||||
|
- Added `format_relative_time()` helper
|
||||||
|
|
||||||
|
### Iteration 7: Registry Instrumentation (Phase 6)
|
||||||
|
- `registry/docker.rs`:
|
||||||
|
- `download_blob()` - record download + cache hit + activity
|
||||||
|
- `get_manifest()` - record download + cache hit + activity
|
||||||
|
- `upload_blob()` - record upload + activity
|
||||||
|
- `put_manifest()` - record upload + activity
|
||||||
|
|
||||||
|
- `registry/npm.rs`:
|
||||||
|
- Cache hit tracking for local storage
|
||||||
|
- Cache miss + proxy fetch tracking
|
||||||
|
|
||||||
|
- `registry/maven.rs`:
|
||||||
|
- `download()` - cache hit/miss + activity
|
||||||
|
- `upload()` - record upload + activity
|
||||||
|
|
||||||
|
- `registry/cargo_registry.rs`:
|
||||||
|
- `download()` - record download + activity
|
||||||
|
|
||||||
|
### Iteration 8: Build & Test
|
||||||
|
- `cargo build` - compiled successfully with minor warnings
|
||||||
|
- Fixed warnings:
|
||||||
|
- Removed unused `RegistryStats` import
|
||||||
|
- Added `#[allow(dead_code)]` to `stat_card()`
|
||||||
|
- `cargo test` - all 75 tests passed
|
||||||
|
|
||||||
|
### Iteration 9: Server Testing
|
||||||
|
- Started server: `cargo run --release --bin nora`
|
||||||
|
- Tested endpoints:
|
||||||
|
```
|
||||||
|
GET /health - OK
|
||||||
|
GET /api/ui/dashboard - returns full metrics JSON
|
||||||
|
GET /ui/ - dark theme dashboard HTML
|
||||||
|
GET /v2/test/manifests/v1 - triggered Docker metrics
|
||||||
|
GET /npm/lodash/-/lodash-4.17.21.tgz - triggered npm proxy metrics
|
||||||
|
```
|
||||||
|
- Verified metrics tracking:
|
||||||
|
- Downloads: 3 (2 Docker + 1 npm)
|
||||||
|
- Cache hit rate: 66.67%
|
||||||
|
- Activity log populated with Pull, ProxyFetch events
|
||||||
|
|
||||||
|
### Iteration 10: Git Commit & Push
|
||||||
|
- Staged 11 files (2 new, 9 modified)
|
||||||
|
- Commit: `93f9655 Add dashboard metrics, activity log, and dark theme`
|
||||||
|
- Pushed to `origin/main`
|
||||||
|
|
||||||
|
### Iteration 11: Documentation
|
||||||
|
- Updated `TODO.md` with v0.2.1 section
|
||||||
|
- Created this `SESSION_NOTES.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Key Decisions Made
|
||||||
|
1. **In-memory metrics** - AtomicU64 for thread-safety, reset on restart
|
||||||
|
2. **Bounded activity log** - 50 entries max, oldest evicted
|
||||||
|
3. **Polling over WebSocket** - simpler, 5-second interval sufficient
|
||||||
|
4. **Dark theme only for dashboard** - registry list pages keep light theme
|
||||||
|
|
||||||
|
### Files Changed Summary
|
||||||
|
```
|
||||||
|
New:
|
||||||
|
nora-registry/src/activity_log.rs
|
||||||
|
nora-registry/src/dashboard_metrics.rs
|
||||||
|
|
||||||
|
Modified:
|
||||||
|
nora-registry/src/main.rs (+8 lines)
|
||||||
|
nora-registry/src/registry/cargo_registry.rs (+13 lines)
|
||||||
|
nora-registry/src/registry/docker.rs (+47 lines)
|
||||||
|
nora-registry/src/registry/maven.rs (+36 lines)
|
||||||
|
nora-registry/src/registry/npm.rs (+29 lines)
|
||||||
|
nora-registry/src/ui/api.rs (+154 lines)
|
||||||
|
nora-registry/src/ui/components.rs (+394 lines)
|
||||||
|
nora-registry/src/ui/mod.rs (+5 lines)
|
||||||
|
nora-registry/src/ui/templates.rs (+180/-79 lines)
|
||||||
|
|
||||||
|
Total: ~1004 insertions, 79 deletions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Useful Commands
|
||||||
|
```bash
|
||||||
|
# Start server
|
||||||
|
cargo run --release --bin nora
|
||||||
|
|
||||||
|
# Test dashboard
|
||||||
|
curl http://127.0.0.1:4000/api/ui/dashboard
|
||||||
|
|
||||||
|
# View UI
|
||||||
|
open http://127.0.0.1:4000/ui/
|
||||||
|
|
||||||
|
# Trigger metrics
|
||||||
|
curl http://127.0.0.1:4000/v2/test/manifests/v1
|
||||||
|
curl http://127.0.0.1:4000/npm/lodash/-/lodash-4.17.21.tgz -o /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
63
TODO.md
63
TODO.md
@@ -11,6 +11,69 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v0.2.1 - Dashboard Expansion (2026-01-26) - DONE
|
||||||
|
|
||||||
|
### Commit: 93f9655
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `nora-registry/src/dashboard_metrics.rs` - AtomicU64 counters for metrics
|
||||||
|
- `nora-registry/src/activity_log.rs` - Bounded activity log (50 entries)
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `nora-registry/src/main.rs` - Added modules, updated AppState
|
||||||
|
- `nora-registry/src/ui/api.rs` - Added DashboardResponse, api_dashboard()
|
||||||
|
- `nora-registry/src/ui/mod.rs` - Added /api/ui/dashboard route
|
||||||
|
- `nora-registry/src/ui/components.rs` - Dark theme components
|
||||||
|
- `nora-registry/src/ui/templates.rs` - New render_dashboard()
|
||||||
|
- `nora-registry/src/registry/docker.rs` - Instrumented handlers
|
||||||
|
- `nora-registry/src/registry/npm.rs` - Instrumented with cache tracking
|
||||||
|
- `nora-registry/src/registry/maven.rs` - Instrumented download/upload
|
||||||
|
- `nora-registry/src/registry/cargo_registry.rs` - Instrumented download
|
||||||
|
|
||||||
|
### Features Implemented
|
||||||
|
- [x] Global stats panel (downloads, uploads, artifacts, cache hit %, storage)
|
||||||
|
- [x] Per-registry metrics (Docker, Maven, npm, Cargo, PyPI)
|
||||||
|
- [x] Mount points table with proxy upstreams
|
||||||
|
- [x] Activity log (last 20 events)
|
||||||
|
- [x] Dark theme (#0f172a background, #1e293b cards)
|
||||||
|
- [x] Auto-refresh polling (5 seconds)
|
||||||
|
- [x] Cache hit/miss tracking
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
- `GET /api/ui/dashboard` - Full dashboard data as JSON
|
||||||
|
|
||||||
|
### Dark Theme Colors
|
||||||
|
```
|
||||||
|
Background: #0f172a (slate-950)
|
||||||
|
Cards: #1e293b (slate-800)
|
||||||
|
Borders: slate-700
|
||||||
|
Text primary: slate-200
|
||||||
|
Text secondary: slate-400
|
||||||
|
Accent: blue-400
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Commands
|
||||||
|
```bash
|
||||||
|
# Test dashboard API
|
||||||
|
curl http://127.0.0.1:4000/api/ui/dashboard
|
||||||
|
|
||||||
|
# Test Docker pull (triggers metrics)
|
||||||
|
curl http://127.0.0.1:4000/v2/test/manifests/v1
|
||||||
|
|
||||||
|
# Test npm proxy (triggers cache miss)
|
||||||
|
curl http://127.0.0.1:4000/npm/lodash/-/lodash-4.17.21.tgz -o /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
### Future Improvements (Dashboard)
|
||||||
|
- [ ] Add PyPI download instrumentation
|
||||||
|
- [ ] Persist metrics to disk (currently reset on restart)
|
||||||
|
- [ ] Add WebSocket for real-time updates (instead of polling)
|
||||||
|
- [ ] Add graphs/charts for metrics over time
|
||||||
|
- [ ] Add user/client tracking in activity log
|
||||||
|
- [ ] Dark/light theme toggle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.3.0 - OIDC / Workload Identity Federation
|
## v0.3.0 - OIDC / Workload Identity Federation
|
||||||
|
|
||||||
### Killer Feature: OIDC for CI/CD
|
### Killer Feature: OIDC for CI/CD
|
||||||
|
|||||||
@@ -71,12 +71,7 @@ impl ActivityLog {
|
|||||||
/// Get the most recent N entries (newest first)
|
/// Get the most recent N entries (newest first)
|
||||||
pub fn recent(&self, count: usize) -> Vec<ActivityEntry> {
|
pub fn recent(&self, count: usize) -> Vec<ActivityEntry> {
|
||||||
let entries = self.entries.read();
|
let entries = self.entries.read();
|
||||||
entries
|
entries.iter().rev().take(count).cloned().collect()
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.take(count)
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all entries (newest first)
|
/// Get all entries (newest first)
|
||||||
|
|||||||
@@ -21,7 +21,15 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
|
|||||||
let key = format!("maven/{}", path);
|
let key = format!("maven/{}", path);
|
||||||
|
|
||||||
// Extract artifact name for logging (last 2-3 path components)
|
// 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("/");
|
let artifact_name = path
|
||||||
|
.split('/')
|
||||||
|
.rev()
|
||||||
|
.take(3)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
|
||||||
// Try local storage first
|
// Try local storage first
|
||||||
if let Ok(data) = state.storage.get(&key).await {
|
if let Ok(data) = state.storage.get(&key).await {
|
||||||
@@ -76,7 +84,15 @@ async fn upload(
|
|||||||
let key = format!("maven/{}", path);
|
let key = format!("maven/{}", path);
|
||||||
|
|
||||||
// Extract artifact name for logging
|
// Extract artifact name for logging
|
||||||
let artifact_name = path.split('/').rev().take(3).collect::<Vec<_>>().into_iter().rev().collect::<Vec<_>>().join("/");
|
let artifact_name = path
|
||||||
|
.split('/')
|
||||||
|
.rev()
|
||||||
|
.take(3)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
|
||||||
match state.storage.put(&key, &body).await {
|
match state.storage.put(&key, &body).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
|||||||
@@ -139,8 +139,11 @@ pub async fn api_dashboard(State(state): State<Arc<AppState>>) -> Json<Dashboard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_artifacts = registry_stats.docker + registry_stats.maven +
|
let total_artifacts = registry_stats.docker
|
||||||
registry_stats.npm + registry_stats.cargo + registry_stats.pypi;
|
+ registry_stats.maven
|
||||||
|
+ registry_stats.npm
|
||||||
|
+ registry_stats.cargo
|
||||||
|
+ registry_stats.pypi;
|
||||||
|
|
||||||
let global_stats = GlobalStats {
|
let global_stats = GlobalStats {
|
||||||
downloads: state.metrics.downloads.load(Ordering::Relaxed),
|
downloads: state.metrics.downloads.load(Ordering::Relaxed),
|
||||||
|
|||||||
@@ -61,7 +61,12 @@ pub fn layout(title: &str, content: &str, active_page: Option<&str>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Dark theme layout wrapper for dashboard
|
/// Dark theme layout wrapper for dashboard
|
||||||
pub fn layout_dark(title: &str, content: &str, active_page: Option<&str>, extra_scripts: &str) -> String {
|
pub fn layout_dark(
|
||||||
|
title: &str,
|
||||||
|
content: &str,
|
||||||
|
active_page: Option<&str>,
|
||||||
|
extra_scripts: &str,
|
||||||
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
r##"<!DOCTYPE html>
|
r##"<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -238,7 +243,13 @@ fn header_dark() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render global stats row (5-column grid)
|
/// Render global stats row (5-column grid)
|
||||||
pub fn render_global_stats(downloads: u64, uploads: u64, artifacts: u64, cache_hit_percent: f64, storage_bytes: u64) -> String {
|
pub fn render_global_stats(
|
||||||
|
downloads: u64,
|
||||||
|
uploads: u64,
|
||||||
|
artifacts: u64,
|
||||||
|
cache_hit_percent: f64,
|
||||||
|
storage_bytes: u64,
|
||||||
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
r##"
|
r##"
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
|
||||||
@@ -273,7 +284,15 @@ pub fn render_global_stats(downloads: u64, uploads: u64, artifacts: u64, cache_h
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render registry card with extended metrics
|
/// Render registry card with extended metrics
|
||||||
pub fn render_registry_card(name: &str, icon_path: &str, artifact_count: usize, downloads: u64, uploads: u64, size_bytes: u64, href: &str) -> String {
|
pub fn render_registry_card(
|
||||||
|
name: &str,
|
||||||
|
icon_path: &str,
|
||||||
|
artifact_count: usize,
|
||||||
|
downloads: u64,
|
||||||
|
uploads: u64,
|
||||||
|
size_bytes: u64,
|
||||||
|
href: &str,
|
||||||
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
r##"
|
r##"
|
||||||
<a href="{}" id="registry-{}" class="block bg-[#1e293b] rounded-lg border border-slate-700 p-4 md:p-6 hover:border-blue-400 transition-all">
|
<a href="{}" id="registry-{}" class="block bg-[#1e293b] rounded-lg border border-slate-700 p-4 md:p-6 hover:border-blue-400 transition-all">
|
||||||
@@ -359,7 +378,13 @@ pub fn render_mount_points_table(mount_points: &[(String, String, Option<String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render a single activity log row
|
/// Render a single activity log row
|
||||||
pub fn render_activity_row(timestamp: &str, action: &str, artifact: &str, registry: &str, source: &str) -> String {
|
pub fn render_activity_row(
|
||||||
|
timestamp: &str,
|
||||||
|
action: &str,
|
||||||
|
artifact: &str,
|
||||||
|
registry: &str,
|
||||||
|
source: &str,
|
||||||
|
) -> String {
|
||||||
let action_color = match action {
|
let action_color = match action {
|
||||||
"PULL" => "text-blue-400",
|
"PULL" => "text-blue-400",
|
||||||
"PUSH" => "text-green-400",
|
"PUSH" => "text-green-400",
|
||||||
@@ -378,7 +403,12 @@ pub fn render_activity_row(timestamp: &str, action: &str, artifact: &str, regist
|
|||||||
<td class="py-2 text-slate-500">{}</td>
|
<td class="py-2 text-slate-500">{}</td>
|
||||||
</tr>
|
</tr>
|
||||||
"##,
|
"##,
|
||||||
timestamp, action_color, action, html_escape(artifact), registry, source
|
timestamp,
|
||||||
|
action_color,
|
||||||
|
action,
|
||||||
|
html_escape(artifact),
|
||||||
|
registry,
|
||||||
|
source
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,37 +13,49 @@ pub fn render_dashboard(data: &DashboardResponse) -> String {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Render registry cards
|
// Render registry cards
|
||||||
let registry_cards: String = data.registry_stats.iter().map(|r| {
|
let registry_cards: String = data
|
||||||
let icon = match r.name.as_str() {
|
.registry_stats
|
||||||
"docker" => icons::DOCKER,
|
.iter()
|
||||||
"maven" => icons::MAVEN,
|
.map(|r| {
|
||||||
"npm" => icons::NPM,
|
let icon = match r.name.as_str() {
|
||||||
"cargo" => icons::CARGO,
|
"docker" => icons::DOCKER,
|
||||||
"pypi" => icons::PYPI,
|
"maven" => icons::MAVEN,
|
||||||
_ => icons::DOCKER,
|
"npm" => icons::NPM,
|
||||||
};
|
"cargo" => icons::CARGO,
|
||||||
let display_name = match r.name.as_str() {
|
"pypi" => icons::PYPI,
|
||||||
"docker" => "Docker",
|
_ => icons::DOCKER,
|
||||||
"maven" => "Maven",
|
};
|
||||||
"npm" => "npm",
|
let display_name = match r.name.as_str() {
|
||||||
"cargo" => "Cargo",
|
"docker" => "Docker",
|
||||||
"pypi" => "PyPI",
|
"maven" => "Maven",
|
||||||
_ => &r.name,
|
"npm" => "npm",
|
||||||
};
|
"cargo" => "Cargo",
|
||||||
render_registry_card(
|
"pypi" => "PyPI",
|
||||||
display_name,
|
_ => &r.name,
|
||||||
icon,
|
};
|
||||||
r.artifact_count,
|
render_registry_card(
|
||||||
r.downloads,
|
display_name,
|
||||||
r.uploads,
|
icon,
|
||||||
r.size_bytes,
|
r.artifact_count,
|
||||||
&format!("/ui/{}", r.name),
|
r.downloads,
|
||||||
)
|
r.uploads,
|
||||||
}).collect();
|
r.size_bytes,
|
||||||
|
&format!("/ui/{}", r.name),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Render mount points
|
// Render mount points
|
||||||
let mount_data: Vec<(String, String, Option<String>)> = data.mount_points.iter()
|
let mount_data: Vec<(String, String, Option<String>)> = data
|
||||||
.map(|m| (m.registry.clone(), m.mount_path.clone(), m.proxy_upstream.clone()))
|
.mount_points
|
||||||
|
.iter()
|
||||||
|
.map(|m| {
|
||||||
|
(
|
||||||
|
m.registry.clone(),
|
||||||
|
m.mount_path.clone(),
|
||||||
|
m.proxy_upstream.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mount_points = render_mount_points_table(&mount_data);
|
let mount_points = render_mount_points_table(&mount_data);
|
||||||
|
|
||||||
@@ -51,16 +63,19 @@ pub fn render_dashboard(data: &DashboardResponse) -> String {
|
|||||||
let activity_rows: String = if data.activity.is_empty() {
|
let activity_rows: String = if data.activity.is_empty() {
|
||||||
r##"<tr><td colspan="5" class="py-8 text-center text-slate-500">No recent activity</td></tr>"##.to_string()
|
r##"<tr><td colspan="5" class="py-8 text-center text-slate-500">No recent activity</td></tr>"##.to_string()
|
||||||
} else {
|
} else {
|
||||||
data.activity.iter().map(|entry| {
|
data.activity
|
||||||
let time_ago = format_relative_time(&entry.timestamp);
|
.iter()
|
||||||
render_activity_row(
|
.map(|entry| {
|
||||||
&time_ago,
|
let time_ago = format_relative_time(&entry.timestamp);
|
||||||
&entry.action.to_string(),
|
render_activity_row(
|
||||||
&entry.artifact,
|
&time_ago,
|
||||||
&entry.registry,
|
&entry.action.to_string(),
|
||||||
&entry.source,
|
&entry.artifact,
|
||||||
)
|
&entry.registry,
|
||||||
}).collect()
|
&entry.source,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
};
|
};
|
||||||
let activity_log = render_activity_log(&activity_rows);
|
let activity_log = render_activity_log(&activity_rows);
|
||||||
|
|
||||||
@@ -95,11 +110,7 @@ pub fn render_dashboard(data: &DashboardResponse) -> String {
|
|||||||
{}
|
{}
|
||||||
</div>
|
</div>
|
||||||
"##,
|
"##,
|
||||||
uptime_str,
|
uptime_str, global_stats, registry_cards, mount_points, activity_log,
|
||||||
global_stats,
|
|
||||||
registry_cards,
|
|
||||||
mount_points,
|
|
||||||
activity_log,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let polling_script = render_polling_script();
|
let polling_script = render_polling_script();
|
||||||
|
|||||||
Reference in New Issue
Block a user