Alingment
This commit is contained in:
parent
2819df185e
commit
3a87c7a011
9 changed files with 325 additions and 78 deletions
97
Cargo.lock
generated
97
Cargo.lock
generated
|
|
@ -17,6 +17,15 @@ version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.21"
|
version = "0.6.21"
|
||||||
|
|
@ -79,6 +88,12 @@ version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
@ -150,6 +165,19 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.53"
|
version = "4.5.53"
|
||||||
|
|
@ -782,6 +810,30 @@ dependencies = [
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
@ -1099,6 +1151,15 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
|
|
@ -1983,6 +2044,7 @@ name = "uptime-kuma-dashboard"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
"fluent-templates",
|
"fluent-templates",
|
||||||
|
|
@ -2159,6 +2221,41 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ once_cell = "1.19"
|
||||||
url = "2.5.7"
|
url = "2.5.7"
|
||||||
ratatui = "0.29.0"
|
ratatui = "0.29.0"
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
|
chrono = "0.4.42"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::core::models::{UnifiedData, UnifiedMonitorData};
|
use crate::core::models::{UnifiedData, UnifiedGroupData, UnifiedMonitorData};
|
||||||
use crate::data::heartbeat::model::HeartbeatResponse;
|
use crate::data::heartbeat::model::HeartbeatResponse;
|
||||||
use crate::data::status_page::model::StatusPageResponse;
|
use crate::data::status_page::model::StatusPageResponse;
|
||||||
|
|
||||||
pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatResponse) -> UnifiedData {
|
pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatResponse) -> UnifiedData {
|
||||||
let mut monitors = Vec::new();
|
let mut groups = Vec::new();
|
||||||
|
|
||||||
for group in &status_page.public_group_list {
|
for group in &status_page.public_group_list {
|
||||||
|
let mut monitors = Vec::new();
|
||||||
for monitor_info in &group.monitor_list {
|
for monitor_info in &group.monitor_list {
|
||||||
let uptime_data = heartbeat.get_uptime(monitor_info.id, 24).cloned();
|
let uptime_data = heartbeat.get_uptime(monitor_info.id, 24).cloned();
|
||||||
|
|
||||||
|
|
@ -22,14 +23,21 @@ pub fn unify_data(status_page: &StatusPageResponse, heartbeat: &HeartbeatRespons
|
||||||
uptime_data,
|
uptime_data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
monitors.sort_by_key(|m| m.monitor_info.name.clone());
|
||||||
|
|
||||||
|
groups.push(UnifiedGroupData {
|
||||||
|
group_info: group.clone(),
|
||||||
|
monitors,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
monitors.sort_by_key(|m| m.monitor_info.id);
|
groups.sort_by_key(|g| g.group_info.weight);
|
||||||
|
|
||||||
UnifiedData {
|
UnifiedData {
|
||||||
title: status_page.config.title.clone(),
|
title: status_page.config.title.clone(),
|
||||||
description: status_page.config.description.clone(),
|
description: status_page.config.description.clone(),
|
||||||
monitors,
|
groups,
|
||||||
audo_refresh_interval: status_page.config.auto_refresh_interval,
|
auto_refresh_interval: status_page.config.auto_refresh_interval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::data::heartbeat::model::{HeartbeatEntry, UptimeData};
|
use crate::data::heartbeat::model::{HeartbeatEntry, UptimeData};
|
||||||
use crate::data::status_page::model::MonitorInfo;
|
use crate::data::status_page::model::{MonitorInfo, StatusPageGroup};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UnifiedMonitorData {
|
pub struct UnifiedMonitorData {
|
||||||
|
|
@ -12,6 +12,12 @@ pub struct UnifiedMonitorData {
|
||||||
pub struct UnifiedData {
|
pub struct UnifiedData {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub monitors: Vec<UnifiedMonitorData>,
|
pub auto_refresh_interval: u32,
|
||||||
pub audo_refresh_interval: u32,
|
pub groups: Vec<UnifiedGroupData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UnifiedGroupData {
|
||||||
|
pub group_info: StatusPageGroup,
|
||||||
|
pub monitors: Vec<UnifiedMonitorData>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::api::{UptimeKumaClient, UptimeKumaEndpoints};
|
use crate::api::{UptimeKumaClient, UptimeKumaEndpoints};
|
||||||
use crate::core;
|
use crate::core;
|
||||||
use crate::i18n::{t, t_with_args};
|
use crate::i18n::{t, t_with_args};
|
||||||
|
use crate::ui::components::render_footer;
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
components::{render_header, render_monitor_list},
|
components::{render_header, render_monitor_list},
|
||||||
dashboard::model::DashboardViewState,
|
dashboard::model::DashboardViewState,
|
||||||
|
|
@ -110,7 +111,11 @@ impl App {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(1)
|
.margin(1)
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(1)])
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
render_header(frame, chunks[0], &self.state);
|
render_header(frame, chunks[0], &self.state);
|
||||||
|
|
@ -119,11 +124,12 @@ impl App {
|
||||||
Self::render_loading(frame, chunks[1]);
|
Self::render_loading(frame, chunks[1]);
|
||||||
} else if let Some(error) = &self.state.error_message {
|
} else if let Some(error) = &self.state.error_message {
|
||||||
Self::render_error(frame, chunks[1], error);
|
Self::render_error(frame, chunks[1], error);
|
||||||
} else if self.state.monitors.is_empty() {
|
} else if self.state.groups.is_empty() || self.state.get_all_monitors().is_empty() {
|
||||||
Self::render_no_data(frame, chunks[1]);
|
Self::render_no_data(frame, chunks[1]);
|
||||||
} else {
|
} else {
|
||||||
render_monitor_list(frame, chunks[1], &self.state);
|
render_monitor_list(frame, chunks[1], &self.state);
|
||||||
}
|
}
|
||||||
|
render_footer(frame, chunks[2]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
27
src/ui/components/footer.rs
Normal file
27
src/ui/components/footer.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
|
layout::{Alignment, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::Span,
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
};
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
|
pub fn render_footer(frame: &mut Frame, area: Rect) {
|
||||||
|
let now = Local::now();
|
||||||
|
let datatime_str = now.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
|
||||||
|
let footer = Paragraph::new(Span::styled(
|
||||||
|
datatime_str,
|
||||||
|
Style::default().fg(Color::Gray).add_modifier(Modifier::DIM),
|
||||||
|
))
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_style(Style::default().fg(Color::Blue)),
|
||||||
|
)
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
|
||||||
|
frame.render_widget(footer, area);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
pub mod footer;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
pub mod monitor_list;
|
pub mod monitor_list;
|
||||||
|
|
||||||
|
pub use footer::render_footer;
|
||||||
pub use header::render_header;
|
pub use header::render_header;
|
||||||
pub use monitor_list::render_monitor_list;
|
pub use monitor_list::render_monitor_list;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
use crate::ui::dashboard::model::DashboardViewState;
|
use crate::ui::dashboard::model::{DashboardViewState, GroupViewState};
|
||||||
use crate::ui::dashboard::{MonitorStatus, MonitorViewState};
|
use crate::ui::dashboard::{MonitorStatus, MonitorViewState};
|
||||||
|
use ratatui::layout::Layout;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::{Constraint, Rect},
|
layout::{Alignment, Constraint, Direction, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Row, Table},
|
widgets::{Block, Borders, Row, Table},
|
||||||
|
|
@ -13,42 +16,110 @@ const STATUS_LINE_LENGTH: usize = 100;
|
||||||
const MAX_NAME_LENGTH: usize = 30;
|
const MAX_NAME_LENGTH: usize = 30;
|
||||||
|
|
||||||
pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) {
|
pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) {
|
||||||
let block = Block::default()
|
let group_areas = layout_groups(area, state.groups.len());
|
||||||
.title(t("monitors"))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default().fg(Color::Blue));
|
|
||||||
|
|
||||||
let header = vec![
|
for (i, group) in state.groups.iter().enumerate() {
|
||||||
|
if i < group_areas.len() {
|
||||||
|
render_group(frame, group_areas[i], group, i == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_groups(area: Rect, group_count: usize) -> Vec<Rect> {
|
||||||
|
if group_count == 0 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let height_per_group = (area.height as usize / group_count).max(3);
|
||||||
|
let mut current_y = area.y;
|
||||||
|
let mut areas = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..group_count {
|
||||||
|
if current_y + height_per_group as u16 > area.y + area.height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
areas.push(Rect {
|
||||||
|
x: area.x,
|
||||||
|
y: current_y,
|
||||||
|
width: area.width,
|
||||||
|
height: height_per_group as u16,
|
||||||
|
});
|
||||||
|
|
||||||
|
current_y += height_per_group as u16;
|
||||||
|
}
|
||||||
|
areas
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first: bool) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(0)
|
||||||
|
.constraints([Constraint::Length(1), Constraint::Min(1)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let group_title = Line::from(vec![
|
||||||
|
Span::styled(format!("{} ", group.name), title_style()),
|
||||||
|
Span::styled(
|
||||||
|
format!("({})", group.monitors.len()),
|
||||||
|
Style::default().fg(Color::Gray),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let title_block = Block::default()
|
||||||
|
.borders(if is_first {
|
||||||
|
Borders::TOP | Borders::LEFT | Borders::RIGHT
|
||||||
|
} else {
|
||||||
|
Borders::ALL
|
||||||
|
})
|
||||||
|
.border_style(Style::default().fg(Color::Blue))
|
||||||
|
.title(group_title)
|
||||||
|
.title_alignment(Alignment::Left);
|
||||||
|
|
||||||
|
frame.render_widget(title_block, chunks[0]);
|
||||||
|
|
||||||
|
if !group.monitors.is_empty() {
|
||||||
|
render_monitor_table(frame, chunks[1], &group.monitors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_monitor_table(frame: &mut Frame, area: Rect, monitors: &Vec<MonitorViewState>) {
|
||||||
|
let max_items = area.height as usize;
|
||||||
|
let items_to_show = min(monitors.len(), max_items);
|
||||||
|
|
||||||
|
let header_cells = vec![
|
||||||
"".to_string(),
|
"".to_string(),
|
||||||
t("monitor"),
|
t("monitor"),
|
||||||
t("Response"),
|
t("Response"),
|
||||||
t("uptime"),
|
t("uptime"),
|
||||||
t("history"),
|
t("history"),
|
||||||
];
|
];
|
||||||
let constraints = vec![
|
|
||||||
|
let header = Row::new(header_cells).style(title_style()).height(1);
|
||||||
|
|
||||||
|
let rows: Vec<Row> = monitors
|
||||||
|
.iter()
|
||||||
|
.take(items_to_show)
|
||||||
|
.map(|monitor| create_monitor_item(monitor))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let widths = vec![
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
Constraint::Length(MAX_NAME_LENGTH as u16),
|
Constraint::Length(MAX_NAME_LENGTH as u16),
|
||||||
Constraint::Length(10),
|
Constraint::Length(10),
|
||||||
Constraint::Length(10),
|
Constraint::Length(10),
|
||||||
Constraint::Length(STATUS_LINE_LENGTH as u16 + 8),
|
Constraint::Length(STATUS_LINE_LENGTH as u16),
|
||||||
];
|
];
|
||||||
|
|
||||||
let available_height = area.height.saturating_sub(2);
|
let table = Table::new(rows, widths)
|
||||||
let visible_items = state.monitors.len().min(available_height as usize);
|
.header(header)
|
||||||
|
.block(
|
||||||
let rows: Vec<Row> = state
|
Block::default()
|
||||||
.monitors
|
.borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
|
||||||
.iter()
|
.border_style(Style::default().fg(Color::Blue)),
|
||||||
.take(visible_items)
|
)
|
||||||
.map(|m| create_monitor_item(m))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let table = Table::new(rows, constraints)
|
|
||||||
.header(Row::new(header))
|
|
||||||
.block(block)
|
|
||||||
.column_spacing(1)
|
.column_spacing(1)
|
||||||
.highlight_symbol(">> ")
|
.style(Style::default());
|
||||||
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED));
|
|
||||||
|
|
||||||
frame.render_widget(table, area);
|
frame.render_widget(table, area);
|
||||||
}
|
}
|
||||||
|
|
@ -126,3 +197,9 @@ fn create_status_line_spans(status_history: &[MonitorStatus]) -> Line<'_> {
|
||||||
}
|
}
|
||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn title_style() -> Style {
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Yellow)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::core::models::UnifiedData;
|
use crate::core::models::{UnifiedData, UnifiedGroupData};
|
||||||
use crate::data::heartbeat::model::HeartbeatEntry;
|
use crate::data::heartbeat::model::HeartbeatEntry;
|
||||||
use crate::i18n::t;
|
use crate::i18n::t;
|
||||||
|
|
||||||
|
|
@ -21,11 +21,17 @@ pub struct MonitorViewState {
|
||||||
pub status_history: Vec<MonitorStatus>,
|
pub status_history: Vec<MonitorStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GroupViewState {
|
||||||
|
pub name: String,
|
||||||
|
pub monitors: Vec<MonitorViewState>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DashboardViewState {
|
pub struct DashboardViewState {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub descriptions: Option<String>,
|
pub descriptions: Option<String>,
|
||||||
pub monitors: Vec<MonitorViewState>,
|
pub groups: Vec<GroupViewState>,
|
||||||
pub is_loading: bool,
|
pub is_loading: bool,
|
||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
pub auto_refresh_interval: u32,
|
pub auto_refresh_interval: u32,
|
||||||
|
|
@ -36,7 +42,7 @@ impl DashboardViewState {
|
||||||
Self {
|
Self {
|
||||||
title: t("loading").to_string(),
|
title: t("loading").to_string(),
|
||||||
descriptions: None,
|
descriptions: None,
|
||||||
monitors: Vec::new(),
|
groups: Vec::new(),
|
||||||
is_loading: true,
|
is_loading: true,
|
||||||
error_message: None,
|
error_message: None,
|
||||||
auto_refresh_interval: 300,
|
auto_refresh_interval: 300,
|
||||||
|
|
@ -44,9 +50,54 @@ impl DashboardViewState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_unified_data(data: UnifiedData) -> Self {
|
pub fn from_unified_data(data: UnifiedData) -> Self {
|
||||||
|
let mut groups = Vec::new();
|
||||||
|
|
||||||
|
for group in data.groups {
|
||||||
|
groups.push(GroupViewState {
|
||||||
|
name: group.group_info.name.clone(),
|
||||||
|
monitors: add_monitor_view_state(group),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
title: data.title,
|
||||||
|
descriptions: data.description,
|
||||||
|
groups,
|
||||||
|
is_loading: false,
|
||||||
|
error_message: None,
|
||||||
|
auto_refresh_interval: data.auto_refresh_interval.max(30),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_monitors(&self) -> Vec<&MonitorViewState> {
|
||||||
|
self.groups.iter().flat_map(|g| g.monitors.iter()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
|
||||||
|
let mut history = heartbeats
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take(100)
|
||||||
|
.map(|h| match h.status {
|
||||||
|
0 => MonitorStatus::Down,
|
||||||
|
1 => MonitorStatus::Up,
|
||||||
|
_ => MonitorStatus::Unknown,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
while history.len() < 100 {
|
||||||
|
history.push(MonitorStatus::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
history.reverse();
|
||||||
|
history
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_monitor_view_state(group: UnifiedGroupData) -> Vec<MonitorViewState> {
|
||||||
let mut monitors = Vec::new();
|
let mut monitors = Vec::new();
|
||||||
|
|
||||||
for monitor in data.monitors {
|
for monitor in group.monitors {
|
||||||
let status_history = get_status_history(&monitor.heartbeats);
|
let status_history = get_status_history(&monitor.heartbeats);
|
||||||
|
|
||||||
let status = match monitor.heartbeats.last().map(|h| h.status) {
|
let status = match monitor.heartbeats.last().map(|h| h.status) {
|
||||||
|
|
@ -76,7 +127,7 @@ impl DashboardViewState {
|
||||||
monitors.push(MonitorViewState {
|
monitors.push(MonitorViewState {
|
||||||
id: monitor.monitor_info.id,
|
id: monitor.monitor_info.id,
|
||||||
name: monitor.monitor_info.name,
|
name: monitor.monitor_info.name,
|
||||||
group_name: "Services".to_string(),
|
group_name: group.group_info.name.clone(),
|
||||||
status,
|
status,
|
||||||
response_time,
|
response_time,
|
||||||
uptime_24h,
|
uptime_24h,
|
||||||
|
|
@ -87,33 +138,5 @@ impl DashboardViewState {
|
||||||
|
|
||||||
monitors.sort_by_key(|m| m.name.clone());
|
monitors.sort_by_key(|m| m.name.clone());
|
||||||
|
|
||||||
Self {
|
monitors
|
||||||
title: data.title,
|
|
||||||
descriptions: data.description,
|
|
||||||
monitors,
|
|
||||||
is_loading: false,
|
|
||||||
error_message: None,
|
|
||||||
auto_refresh_interval: data.audo_refresh_interval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_status_history(heartbeats: &[HeartbeatEntry]) -> Vec<MonitorStatus> {
|
|
||||||
let mut history = heartbeats
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.take(100)
|
|
||||||
.map(|h| match h.status {
|
|
||||||
0 => MonitorStatus::Down,
|
|
||||||
1 => MonitorStatus::Up,
|
|
||||||
_ => MonitorStatus::Unknown,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
while history.len() < 100 {
|
|
||||||
history.push(MonitorStatus::Unknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
history.reverse();
|
|
||||||
history
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue