mod actions;
mod cache;
mod refresh;
mod stats;
use timeweb_rs::authenticated;
use self::{
actions::{detail_context, fetch_detail_extra, fetch_drill, perform_action, perform_create},
refresh::{spawn_one_shot_refresh, spawn_refresh_loop},
stats::spawn_stats_fetch
};
use crate::{config::AppConfig, error::TwcError, tui};
fn persist_dashboard_prefs(app: &tui::app::App) {
let Ok(mut cfg) = AppConfig::load() else {
return;
};
cfg.theme = app.theme;
cfg.language = app.language;
cfg.dashboard.hidden_widgets = app.hidden_widget_ids();
cfg.dashboard.list_width_pct = app.list_width_pct;
cfg.dashboard.hide_empty_tabs = app.hide_empty_tabs;
let _ = cfg.save();
}
#[allow(clippy::too_many_lines)]
pub async fn run_dashboard(
mut token: String,
interval: u64,
theme: crate::tui::themes::Theme,
prefs: crate::config::DashboardPrefs,
profile: Option<String>
) -> Result<(), TwcError> {
use crossterm::{
execute,
terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode
}
};
use ratatui::{Terminal, backend::CrosstermBackend};
use tokio::sync::mpsc;
enable_raw_mode().map_err(|e| TwcError::Io(e.to_string()))?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen).map_err(|e| TwcError::Io(e.to_string()))?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend).map_err(|e| TwcError::Io(e.to_string()))?;
let mut app = tui::app::App::new_with_theme(interval, theme, Some(token.clone()));
app.apply_prefs(
&prefs.hidden_widgets,
prefs.list_width_pct,
prefs.hide_empty_tabs
);
if let Ok(cfg) = AppConfig::load() {
app.language = cfg.language;
let mut names = vec!["default".to_string()];
let mut profile_names: Vec<String> = cfg.profiles.keys().cloned().collect();
profile_names.sort();
names.extend(profile_names);
app.profiles = names;
}
app.active_profile = profile.unwrap_or_else(|| "default".to_string());
app.is_loading = true;
if let Some(snapshot) = cache::load(&app.active_profile) {
app.apply_data(snapshot);
}
terminal
.draw(|f| tui::ui::draw(f, &mut app))
.map_err(|e| TwcError::Io(e.to_string()))?;
let (tx, mut rx) = mpsc::unbounded_channel();
let event_tx = tx.clone();
tokio::spawn(async move {
tui::event::run_event_loop(event_tx).await;
});
let mut refresh_handle = spawn_refresh_loop(tx.clone(), token.clone(), interval);
while let Some(event) = rx.recv().await {
if !tui::event::handle_event(&mut app, event) {
break;
}
if let Ok(size) = terminal.size() {
let sidebar_w = tui::widgets::sidebar::width_for(size.width, app.nav_longest_label());
let content_w = size.width.saturating_sub(sidebar_w).saturating_sub(2);
app.resource_cols =
tui::widgets::card_grid::columns(content_w, app.content_longest_label());
}
terminal
.draw(|f| tui::ui::draw(f, &mut app))
.map_err(|e| TwcError::Io(e.to_string()))?;
if app.prefs_dirty {
persist_dashboard_prefs(&app);
app.prefs_dirty = false;
}
if app.snapshot_dirty {
app.snapshot_dirty = false;
if app.last_load_errors.is_empty() {
cache::save(&app.active_profile, tui::app::DashboardData::from_app(&app));
}
}
if app.refresh_requested {
app.refresh_requested = false;
spawn_one_shot_refresh(tx.clone(), token.clone());
}
if let Some((action_id, action)) = app.take_detail_action() {
match action {
tui::widgets::details::DetailAction::Kind(_) => {}
tui::widgets::details::DetailAction::Redeploy => {
use tui::app::LogLevel;
let config = authenticated(token.clone());
let request = timeweb_rs::models::CreateDeployRequest {
commit_sha: None
};
match timeweb_rs::apis::apps_api::create_deploy(
&config,
&action_id.to_string(),
request
)
.await
{
Ok(_) => {
let msg = format!("redeploy started for app #{action_id}");
app.log(LogLevel::Success, msg.clone());
app.status_message = Some(msg);
}
Err(e) => {
let msg = format!("redeploy failed: {e}");
app.log(LogLevel::Error, msg.clone());
app.error_message = Some(msg);
}
}
}
}
}
if let Some((detail_tab, detail_id)) = app.take_detail_fetch() {
let config = authenticated(token.clone());
let extra_tx = tx.clone();
let context = detail_context(&app, detail_tab, &detail_id);
tokio::spawn(async move {
let sections = fetch_detail_extra(&config, detail_tab, &detail_id, context).await;
if !sections.is_empty() {
let _ = extra_tx.send(tui::event::AppEvent::DetailExtra {
tab: detail_tab,
id: detail_id,
sections
});
}
});
}
if let Some((drill_tab, drill_id, drill_name)) = app.take_drill_request() {
let config = authenticated(token.clone());
let drill_tx = tx.clone();
tokio::spawn(async move {
match fetch_drill(&config, drill_tab, drill_id, &drill_name).await {
Ok(view) => {
let _ = drill_tx.send(tui::event::AppEvent::Drill {
id: drill_id,
view: Box::new(view)
});
}
Err(e) => {
let _ = drill_tx.send(tui::event::AppEvent::DrillFailed {
name: drill_name,
error: e
});
}
}
});
}
if let Some(action) = app.take_dispatch() {
use tui::app::LogLevel;
app.log(
LogLevel::Info,
format!("{} {}", action.kind.label(), action.resource_name)
);
let config = authenticated(token.clone());
perform_action(&config, &mut app, action).await;
spawn_one_shot_refresh(tx.clone(), token.clone());
}
if let Some(form) = app.take_create_request() {
let config = authenticated(token.clone());
perform_create(&config, &mut app, form).await;
spawn_one_shot_refresh(tx.clone(), token.clone());
}
if let Some(profile) = app.take_switch_profile() {
use tui::app::LogLevel;
let lookup = (profile != "default").then_some(profile.as_str());
match AppConfig::load().and_then(|c| c.token_for(lookup)) {
Ok(Some(new_token)) => {
token = new_token;
refresh_handle.abort();
refresh_handle = spawn_refresh_loop(tx.clone(), token.clone(), interval);
app.token = Some(token.clone());
app.active_profile.clone_from(&profile);
app.is_loading = true;
if let Some(snapshot) = cache::load(&profile) {
app.apply_data(snapshot);
}
app.log(
LogLevel::Success,
format!("switched to profile '{profile}'")
);
app.status_message = Some(format!("Profile: {profile}"));
}
_ => {
app.log(
LogLevel::Error,
format!("profile '{profile}' has no token configured")
);
}
}
}
if let Some(req) = app.poll_stats_request() {
spawn_stats_fetch(tx.clone(), token.clone(), req);
}
}
drop(tx);
disable_raw_mode().map_err(|e| TwcError::Io(e.to_string()))?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)
.map_err(|e| TwcError::Io(e.to_string()))?;
terminal
.show_cursor()
.map_err(|e| TwcError::Io(e.to_string()))?;
Ok(())
}