use std::time::Instant;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use crate::config::manager::{ConfigManager, SessionInfo};
use crate::model::loader::Models;
use super::wizard::WizardState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Region {
Global,
China,
Dev,
Other(String),
}
impl Region {
pub fn slug(&self) -> String {
match self {
Region::Global => "global".to_string(),
Region::China => "china".to_string(),
Region::Dev => "dev".to_string(),
Region::Other(s) => s.clone(),
}
}
pub fn from_endpoint(endpoint: Option<&str>) -> Self {
let ep = endpoint.unwrap_or("https://api.cloud.zilliz.com");
if ep.contains("api.cloud.zilliz.com.cn") {
Region::China
} else if ep.contains("api.cloud-uat3.zilliz.com") {
Region::Dev
} else if ep.contains("api.cloud.zilliz.com") {
Region::Global
} else {
let host = ep
.trim_start_matches("https://")
.trim_start_matches("http://")
.split('/')
.next()
.unwrap_or(ep);
Region::Other(host.to_string())
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMethod {
Auth0,
ApiKey,
}
#[derive(Debug, Clone)]
pub enum AuthState {
SignedOut,
SignedIn {
method: AuthMethod,
user: Option<String>,
email: Option<String>,
org: Option<String>,
region: Region,
endpoint: String,
masked_api_key: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct AuthSnapshot {
pub state: AuthState,
pub credentials_path: std::path::PathBuf,
}
impl AuthSnapshot {
pub fn load(config_mgr: &ConfigManager) -> Self {
let session: SessionInfo = config_mgr.current_session();
let state = if session.has_active_credentials {
let region = Region::from_endpoint(session.endpoint.as_deref());
let endpoint = session
.endpoint
.clone()
.unwrap_or_else(|| "https://api.cloud.zilliz.com".to_string());
let method = if session.has_user_section {
AuthMethod::Auth0
} else {
AuthMethod::ApiKey
};
let masked_api_key = match method {
AuthMethod::ApiKey => session
.raw_api_key
.as_deref()
.map(crate::auth::mask_api_key),
AuthMethod::Auth0 => None,
};
AuthState::SignedIn {
method,
user: session.user.clone(),
email: session.email.clone(),
org: session.org.clone(),
region,
endpoint,
masked_api_key,
}
} else {
AuthState::SignedOut
};
Self {
state,
credentials_path: session.credentials_path,
}
}
pub fn is_signed_in(&self) -> bool {
matches!(self.state, AuthState::SignedIn { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Screen {
Home,
SignInRegion,
SignInMethod,
SignInBrowser,
SignInApiKey,
LogoutConfirm,
Help,
}
#[derive(Debug)]
pub enum WizardMsg {
DeviceCodeReady(crate::auth::device_code::DeviceCodeResponse),
DeviceCodeError(String),
LoginExchanged(crate::auth::device_code::LoginPayload),
PollError(String),
ExchangeError(String),
}
#[derive(Debug, Clone)]
pub enum CountCell {
Loading,
Loaded(u64),
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClusterCounts {
pub total: u64,
pub running: u64,
pub suspended: u64,
pub abnormal: u64,
}
#[derive(Debug, Clone)]
pub enum ClusterCountCell {
Loading,
Loaded(ClusterCounts),
Failed(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BillingInfo {
pub balance: f64,
pub last_month: f64,
pub last_month_settled: bool,
}
#[derive(Debug, Clone)]
pub enum BillingCell {
Loading,
Loaded(BillingInfo),
Failed(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct UsageInfo {
pub today: f64,
pub week: f64,
pub month: f64,
}
#[derive(Debug, Clone)]
pub enum UsageCell {
Loading,
Loaded(UsageInfo),
Failed(String),
}
#[derive(Debug, Clone)]
pub struct HomeCounts {
pub projects: CountCell,
pub import_jobs: CountCell,
pub clusters: ClusterCountCell,
pub billing: BillingCell,
pub usage: UsageCell,
pub volumes: CountCell,
pub backups: CountCell,
}
impl HomeCounts {
pub fn loading() -> Self {
Self {
projects: CountCell::Loading,
import_jobs: CountCell::Loading,
clusters: ClusterCountCell::Loading,
billing: BillingCell::Loading,
usage: UsageCell::Loading,
volumes: CountCell::Loading,
backups: CountCell::Loading,
}
}
pub fn any_failed(&self) -> bool {
matches!(self.projects, CountCell::Failed(_))
|| matches!(self.import_jobs, CountCell::Failed(_))
|| matches!(self.clusters, ClusterCountCell::Failed(_))
|| matches!(self.usage, UsageCell::Failed(_))
|| matches!(self.volumes, CountCell::Failed(_))
|| matches!(self.backups, CountCell::Failed(_))
}
}
#[derive(Debug)]
pub enum HomeMsg {
ProjectsLoaded(Result<u64, String>),
ImportJobsLoaded(Result<u64, String>),
ClustersLoaded(Result<ClusterCounts, String>),
BillingLoaded(Result<BillingInfo, String>),
UsageLoaded(Result<UsageInfo, String>),
VolumesLoaded(Result<u64, String>),
BackupsLoaded(Result<u64, String>),
}
pub struct App {
pub models: Models,
pub config_mgr: ConfigManager,
pub should_quit: bool,
pub screen_stack: Vec<Screen>,
pub auth: AuthSnapshot,
pub wizard: Option<WizardState>,
pub msg_tx: UnboundedSender<WizardMsg>,
pub msg_rx: UnboundedReceiver<WizardMsg>,
pub home_counts: HomeCounts,
pub home_tx: UnboundedSender<HomeMsg>,
pub home_rx: UnboundedReceiver<HomeMsg>,
pub home_counts_task: Option<tokio::task::JoinHandle<()>>,
pub home_counts_fetched_at: Option<Instant>,
pub home_counts_pending: usize,
}
const HOME_REFRESH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5 * 60);
const HOME_COUNT_MESSAGES: usize = 7;
impl App {
pub fn new(models: Models, config_mgr: ConfigManager) -> Self {
let auth = AuthSnapshot::load(&config_mgr);
let (tx, rx) = unbounded_channel();
let (home_tx, home_rx) = unbounded_channel();
Self {
models,
config_mgr,
should_quit: false,
screen_stack: vec![Screen::Home],
auth,
wizard: None,
msg_tx: tx,
msg_rx: rx,
home_counts: HomeCounts::loading(),
home_tx,
home_rx,
home_counts_task: None,
home_counts_fetched_at: None,
home_counts_pending: 0,
}
}
pub fn spawn_home_counts_fetch(&mut self) {
if self.home_counts_task.is_some() {
return;
}
if tokio::runtime::Handle::try_current().is_err() {
return;
}
while self.home_rx.try_recv().is_ok() {}
if self.home_counts_fetched_at.is_none() {
self.home_counts = HomeCounts::loading();
}
self.home_counts_pending = HOME_COUNT_MESSAGES;
self.home_counts_fetched_at = Some(Instant::now());
self.home_counts_task = Some(super::home_fetch::spawn(
self.models.clone(),
self.config_mgr.clone(),
self.home_tx.clone(),
));
}
pub fn home_counts_refresh_due(&self) -> bool {
if self.home_counts_task.is_some() {
return false;
}
match self.home_counts_fetched_at {
Some(t) => t.elapsed() >= HOME_REFRESH_INTERVAL,
None => false,
}
}
pub fn abort_home_counts_fetch(&mut self) {
if let Some(handle) = self.home_counts_task.take() {
handle.abort();
}
while self.home_rx.try_recv().is_ok() {}
self.home_counts = HomeCounts::loading();
self.home_counts_fetched_at = None;
self.home_counts_pending = 0;
}
pub fn apply_home_msg(&mut self, msg: HomeMsg) {
match msg {
HomeMsg::ProjectsLoaded(Ok(n)) => self.home_counts.projects = CountCell::Loaded(n),
HomeMsg::ProjectsLoaded(Err(e)) => self.home_counts.projects = CountCell::Failed(e),
HomeMsg::ImportJobsLoaded(Ok(n)) => self.home_counts.import_jobs = CountCell::Loaded(n),
HomeMsg::ImportJobsLoaded(Err(e)) => {
self.home_counts.import_jobs = CountCell::Failed(e)
}
HomeMsg::ClustersLoaded(Ok(c)) => {
self.home_counts.clusters = ClusterCountCell::Loaded(c)
}
HomeMsg::ClustersLoaded(Err(e)) => {
self.home_counts.clusters = ClusterCountCell::Failed(e)
}
HomeMsg::BillingLoaded(Ok(info)) => {
self.home_counts.billing = BillingCell::Loaded(info)
}
HomeMsg::BillingLoaded(Err(e)) => self.home_counts.billing = BillingCell::Failed(e),
HomeMsg::UsageLoaded(Ok(info)) => self.home_counts.usage = UsageCell::Loaded(info),
HomeMsg::UsageLoaded(Err(e)) => self.home_counts.usage = UsageCell::Failed(e),
HomeMsg::VolumesLoaded(Ok(n)) => self.home_counts.volumes = CountCell::Loaded(n),
HomeMsg::VolumesLoaded(Err(e)) => self.home_counts.volumes = CountCell::Failed(e),
HomeMsg::BackupsLoaded(Ok(n)) => self.home_counts.backups = CountCell::Loaded(n),
HomeMsg::BackupsLoaded(Err(e)) => self.home_counts.backups = CountCell::Failed(e),
}
self.home_counts_pending = self.home_counts_pending.saturating_sub(1);
if self.home_counts_pending == 0 {
self.home_counts_task = None;
}
}
pub fn current_screen(&self) -> &Screen {
self.screen_stack.last().unwrap_or(&Screen::Home)
}
pub fn screen_below_top(&self) -> Option<&Screen> {
if self.screen_stack.len() < 2 {
return None;
}
self.screen_stack.get(self.screen_stack.len() - 2)
}
pub fn refresh_auth(&mut self) {
self.auth = AuthSnapshot::load(&self.config_mgr);
}
}