mirror of
https://github.com/moghtech/komodo.git
synced 2026-08-07 21:35:47 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93cce3fb60 |
Generated
+14
-14
@@ -1341,7 +1341,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "command"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"komodo_client",
|
||||
"libc",
|
||||
@@ -1716,7 +1716,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
|
||||
[[package]]
|
||||
name = "database"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
@@ -2018,7 +2018,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -2048,7 +2048,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "environment"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"formatting",
|
||||
@@ -2200,7 +2200,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "formatting"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"mogh_error",
|
||||
]
|
||||
@@ -2450,7 +2450,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command",
|
||||
@@ -3224,7 +3224,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "interpolate"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"komodo_client",
|
||||
@@ -3392,7 +3392,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_cli"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bcrypt",
|
||||
@@ -3422,7 +3422,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_client"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async_timing_util",
|
||||
@@ -3464,7 +3464,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_core"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -3537,7 +3537,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "komodo_periphery"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -4654,7 +4654,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "periphery_client"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"encoding",
|
||||
@@ -7037,7 +7037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "transport"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -8069,7 +8069,7 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.3.0"
|
||||
version = "2.3.1"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://github.com/moghtech/komodo"
|
||||
|
||||
@@ -20,7 +20,10 @@ use komodo_client::{
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
resource::ResourceQuery,
|
||||
stack::{Stack, StackInfo, StackServiceWithUpdate, StackState},
|
||||
stack::{
|
||||
Stack, StackInfo, StackServiceNames, StackServiceWithUpdate,
|
||||
StackState,
|
||||
},
|
||||
update::Update,
|
||||
user::{auto_redeploy_user, stack_user, system_user},
|
||||
},
|
||||
@@ -822,6 +825,11 @@ pub async fn check_stack_for_update_inner(
|
||||
services: extract_services_from_stack(&stack)
|
||||
.into_iter()
|
||||
.map(|service| StackServiceWithUpdate {
|
||||
latest_image: find_latest_image(
|
||||
&service.service_name,
|
||||
&service.image,
|
||||
&stack.info.latest_services,
|
||||
),
|
||||
service: service.service_name,
|
||||
image: service.image,
|
||||
update_available: false,
|
||||
@@ -842,6 +850,11 @@ pub async fn check_stack_for_update_inner(
|
||||
.services
|
||||
.iter()
|
||||
.map(|service| StackServiceWithUpdate {
|
||||
latest_image: find_latest_image(
|
||||
&service.service,
|
||||
&service.image,
|
||||
&stack.info.latest_services,
|
||||
),
|
||||
service: service.service.clone(),
|
||||
image: service.image.clone(),
|
||||
update_available: false,
|
||||
@@ -857,6 +870,11 @@ pub async fn check_stack_for_update_inner(
|
||||
service: service.service.clone(),
|
||||
image: service.image.clone(),
|
||||
update_available: false,
|
||||
latest_image: find_latest_image(
|
||||
&service.service,
|
||||
&service.image,
|
||||
&stack.info.latest_services,
|
||||
),
|
||||
};
|
||||
|
||||
let Some(current_digests) = &service.image_digests else {
|
||||
@@ -1032,6 +1050,22 @@ pub async fn check_stack_for_update_inner(
|
||||
})
|
||||
}
|
||||
|
||||
fn find_latest_image(
|
||||
service_name: &str,
|
||||
current_image: &str,
|
||||
latest_services: &[StackServiceNames],
|
||||
) -> Option<String> {
|
||||
latest_services.iter().find_map(|latest| {
|
||||
if latest.service_name == service_name
|
||||
&& latest.image != current_image
|
||||
{
|
||||
Some(latest.image.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for BatchCheckStackForUpdate {
|
||||
|
||||
@@ -19,7 +19,7 @@ mod monitor;
|
||||
mod network;
|
||||
mod periphery;
|
||||
mod permission;
|
||||
mod reporting;
|
||||
mod report;
|
||||
mod resource;
|
||||
mod schedule;
|
||||
mod stack;
|
||||
@@ -72,7 +72,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
resource::spawn_action_state_refresh_loop();
|
||||
schedule::spawn_schedule_executor();
|
||||
helpers::prune::spawn_prune_loop();
|
||||
reporting::spawn_reporting_loop();
|
||||
report::spawn_reporting_loop();
|
||||
}
|
||||
.instrument(startup_span)
|
||||
.await;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use anyhow::Context;
|
||||
use async_timing_util::{Timelength, wait_until_timelength};
|
||||
use komodo_client::entities::{
|
||||
ResourceTargetVariant, core_report::CoreReport,
|
||||
ResourceTargetVariant, report::KomodoReport,
|
||||
};
|
||||
use mogh_pki::{
|
||||
PkiKind, RotatableKeyPair, SpkiPublicKey,
|
||||
@@ -112,8 +112,9 @@ async fn report(
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let report = CoreReport {
|
||||
report_public_key: keys.public().to_string(),
|
||||
let report = KomodoReport {
|
||||
public_key: keys.public().to_string(),
|
||||
version: String::from(env!("CARGO_PKG_VERSION")),
|
||||
users,
|
||||
count,
|
||||
};
|
||||
@@ -104,6 +104,7 @@ impl super::KomodoResource for Server {
|
||||
state: status.as_ref().map(|s| s.state).unwrap_or_default(),
|
||||
err: status.as_ref().and_then(|s| s.err.clone()),
|
||||
stats: system_stats.map(Into::into),
|
||||
alerting_thresholds: (&server.config).into(),
|
||||
core_count: system_info.and_then(|i| i.core_count),
|
||||
logical_core_count: system_info
|
||||
.and_then(|i| i.logical_core_count),
|
||||
|
||||
@@ -106,33 +106,38 @@ impl super::KomodoResource for Stack {
|
||||
.services
|
||||
.iter()
|
||||
.map(|current_service| {
|
||||
let latest_service = stack
|
||||
.info
|
||||
.latest_services
|
||||
.iter()
|
||||
.find(|latest_service| {
|
||||
current_service.service == latest_service.service_name
|
||||
});
|
||||
let latest_image = if let Some(latest_image) =
|
||||
latest_service.as_ref().map(|s| &s.image)
|
||||
&& latest_image != ¤t_service.image
|
||||
{
|
||||
Some(latest_image.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let update_available = current_service
|
||||
.image_digests
|
||||
.as_ref()
|
||||
.map(|current_digests| {
|
||||
stack
|
||||
.info
|
||||
.latest_services
|
||||
.iter()
|
||||
.find_map(|latest_service| {
|
||||
if current_service.service
|
||||
== latest_service.service_name
|
||||
{
|
||||
latest_service
|
||||
.image_digest
|
||||
.as_ref()?
|
||||
.update_available(current_digests)
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.and_then(|current_digests| {
|
||||
latest_service.as_ref().and_then(|latest_service| {
|
||||
latest_service
|
||||
.image_digest
|
||||
.as_ref()?
|
||||
.update_available(current_digests)
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
StackServiceWithUpdate {
|
||||
service: current_service.service.clone(),
|
||||
image: current_service.image.clone(),
|
||||
latest_image,
|
||||
update_available,
|
||||
}
|
||||
})
|
||||
@@ -239,6 +244,9 @@ impl super::KomodoResource for Stack {
|
||||
branch,
|
||||
latest_hash: stack.info.latest_hash,
|
||||
deployed_hash: stack.info.deployed_hash,
|
||||
auto_update_all_services: stack
|
||||
.config
|
||||
.auto_update_all_services,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ impl StatsClient {
|
||||
let mut network_ingress_bytes: u64 = 0;
|
||||
let mut network_egress_bytes: u64 = 0;
|
||||
|
||||
for (_, network) in self.networks.iter() {
|
||||
for network in self.networks.values() {
|
||||
network_ingress_bytes += network.received();
|
||||
network_egress_bytes += network.transmitted();
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ pub mod build;
|
||||
pub mod builder;
|
||||
/// [core config][config::core] and [periphery config][config::periphery]
|
||||
pub mod config;
|
||||
/// Subtypes of [CoreReport][core_report::CoreReport]
|
||||
pub mod core_report;
|
||||
/// Subtypes of [Deployment][deployment::Deployment].
|
||||
pub mod deployment;
|
||||
/// Networks, Images, Containers.
|
||||
@@ -53,6 +51,8 @@ pub mod procedure;
|
||||
pub mod provider;
|
||||
/// Subtypes of [Repo][repo::Repo].
|
||||
pub mod repo;
|
||||
/// Subtypes of [CoreReport][core_report::CoreReport]
|
||||
pub mod report;
|
||||
/// Subtypes of [Resource][resource::Resource].
|
||||
pub mod resource;
|
||||
/// Subtypes of [Schedule][schedule::Schedule]
|
||||
|
||||
@@ -4,14 +4,16 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::ResourceTargetVariant;
|
||||
|
||||
/// Semi-anonymous core data reporting.
|
||||
/// Reports only include the reporting-specific core public key
|
||||
/// (not the keys used for server connection).
|
||||
/// Semi-anonymous Komodo Core reporting.
|
||||
/// Reports only include the reporting-specific public key
|
||||
/// (not the key pair used for server connection).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct CoreReport {
|
||||
pub struct KomodoReport {
|
||||
/// Reporting specific public key.
|
||||
/// Must match public key obtained through request signature.
|
||||
pub report_public_key: String,
|
||||
pub public_key: String,
|
||||
/// The Komodo Core version string
|
||||
pub version: String,
|
||||
/// The total number of users
|
||||
pub users: u64,
|
||||
/// Resource counts by type
|
||||
@@ -44,6 +44,8 @@ pub struct ServerListItemInfo {
|
||||
pub err: Option<_Serror>,
|
||||
/// System stats, if available
|
||||
pub stats: Option<MinimalSystemStats>,
|
||||
/// The server alerting thresholds
|
||||
pub alerting_thresholds: ServerAlertingThresholds,
|
||||
/// The server's number of physical cores.
|
||||
pub core_count: Option<u32>,
|
||||
/// The server's number of logical cores.
|
||||
@@ -363,6 +365,49 @@ impl utoipa::PartialSchema for PartialServerConfig {
|
||||
#[cfg(feature = "utoipa")]
|
||||
impl utoipa::ToSchema for PartialServerConfig {}
|
||||
|
||||
/// Just the server alerting thresholds
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ServerAlertingThresholds {
|
||||
/// The percentage threshhold which triggers WARNING state for CPU.
|
||||
#[serde(default = "default_cpu_warning")]
|
||||
pub cpu_warning: f32,
|
||||
|
||||
/// The percentage threshhold which triggers CRITICAL state for CPU.
|
||||
#[serde(default = "default_cpu_critical")]
|
||||
pub cpu_critical: f32,
|
||||
|
||||
/// The percentage threshhold which triggers WARNING state for MEM.
|
||||
#[serde(default = "default_mem_warning")]
|
||||
pub mem_warning: f64,
|
||||
|
||||
/// The percentage threshhold which triggers CRITICAL state for MEM.
|
||||
#[serde(default = "default_mem_critical")]
|
||||
pub mem_critical: f64,
|
||||
|
||||
/// The percentage threshhold which triggers WARNING state for DISK.
|
||||
#[serde(default = "default_disk_warning")]
|
||||
pub disk_warning: f64,
|
||||
|
||||
/// The percentage threshhold which triggers CRITICAL state for DISK.
|
||||
#[serde(default = "default_disk_critical")]
|
||||
pub disk_critical: f64,
|
||||
}
|
||||
|
||||
impl From<&ServerConfig> for ServerAlertingThresholds {
|
||||
fn from(config: &ServerConfig) -> Self {
|
||||
ServerAlertingThresholds {
|
||||
cpu_warning: config.cpu_warning,
|
||||
cpu_critical: config.cpu_critical,
|
||||
mem_warning: config.mem_warning,
|
||||
mem_critical: config.mem_critical,
|
||||
disk_warning: config.disk_warning,
|
||||
disk_critical: config.disk_critical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The health of a part of the server.
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
|
||||
@@ -184,6 +184,8 @@ pub struct StackListItemInfo {
|
||||
/// If deployed, will be `deployed_services`.
|
||||
/// Otherwise, its `latest_services`
|
||||
pub services: Vec<StackServiceWithUpdate>,
|
||||
/// Whether stack has auto_update_all_services enabled.
|
||||
pub auto_update_all_services: bool,
|
||||
/// Whether the compose project is missing on the host.
|
||||
/// Ie, it does not show up in `docker compose ls`.
|
||||
/// If true, and the stack is not Down, this is an unhealthy state.
|
||||
@@ -208,8 +210,10 @@ impl StackListItemInfo {
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct StackServiceWithUpdate {
|
||||
pub service: String,
|
||||
/// The service's image
|
||||
/// The service's (current) image
|
||||
pub image: String,
|
||||
/// The latest image (if different than current)
|
||||
pub latest_image: Option<String>,
|
||||
/// Whether there is a newer image available for this service
|
||||
pub update_available: bool,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "komodo_client",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"description": "Komodo client package",
|
||||
"homepage": "https://komo.do",
|
||||
"main": "dist/lib.js",
|
||||
|
||||
@@ -366,8 +366,10 @@ export type BatchCheckDeploymentForUpdateResponse = CheckDeploymentForUpdateResp
|
||||
|
||||
export interface StackServiceWithUpdate {
|
||||
service: string;
|
||||
/** The service's image */
|
||||
/** The service's (current) image */
|
||||
image: string;
|
||||
/** The latest image (if different than current) */
|
||||
latest_image?: string;
|
||||
/** Whether there is a newer image available for this service */
|
||||
update_available: boolean;
|
||||
}
|
||||
@@ -2858,10 +2860,7 @@ export interface SystemInformation {
|
||||
kernel?: string;
|
||||
/** Physical core count */
|
||||
core_count?: number;
|
||||
/**
|
||||
* Logical core count. If available,
|
||||
* used to interpret system load accurately.
|
||||
*/
|
||||
/** Logical core count. */
|
||||
logical_core_count?: number;
|
||||
/** System hostname based off DNS */
|
||||
host_name?: string;
|
||||
@@ -5531,6 +5530,22 @@ export interface MinimalSystemStats {
|
||||
refresh_list_ts: I64;
|
||||
}
|
||||
|
||||
/** Just the server alerting thresholds */
|
||||
export interface ServerAlertingThresholds {
|
||||
/** The percentage threshhold which triggers WARNING state for CPU. */
|
||||
cpu_warning: number;
|
||||
/** The percentage threshhold which triggers CRITICAL state for CPU. */
|
||||
cpu_critical: number;
|
||||
/** The percentage threshhold which triggers WARNING state for MEM. */
|
||||
mem_warning: number;
|
||||
/** The percentage threshhold which triggers CRITICAL state for MEM. */
|
||||
mem_critical: number;
|
||||
/** The percentage threshhold which triggers WARNING state for DISK. */
|
||||
disk_warning: number;
|
||||
/** The percentage threshhold which triggers CRITICAL state for DISK. */
|
||||
disk_critical: number;
|
||||
}
|
||||
|
||||
export interface ServerListItemInfo {
|
||||
/** The server's state. */
|
||||
state: ServerState;
|
||||
@@ -5541,6 +5556,8 @@ export interface ServerListItemInfo {
|
||||
err?: _Serror;
|
||||
/** System stats, if available */
|
||||
stats?: MinimalSystemStats;
|
||||
/** The server alerting thresholds */
|
||||
alerting_thresholds: ServerAlertingThresholds;
|
||||
/** The server's number of physical cores. */
|
||||
core_count?: number;
|
||||
/** The server's number of logical cores. */
|
||||
@@ -5650,6 +5667,8 @@ export interface StackListItemInfo {
|
||||
* Otherwise, its `latest_services`
|
||||
*/
|
||||
services: StackServiceWithUpdate[];
|
||||
/** Whether stack has auto_update_all_services enabled. */
|
||||
auto_update_all_services: boolean;
|
||||
/**
|
||||
* Whether the compose project is missing on the host.
|
||||
* Ie, it does not show up in `docker compose ls`.
|
||||
|
||||
+38
-2
@@ -26,7 +26,7 @@ use komodo_client::entities::{
|
||||
user_group::UserGroup,
|
||||
variable::Variable,
|
||||
};
|
||||
use mongo_indexed::{create_index, create_unique_index};
|
||||
use mongo_indexed::{Document, create_index, create_unique_index};
|
||||
use mungos::{
|
||||
by_id::update_one_by_id,
|
||||
init::MongoBuilder,
|
||||
@@ -90,7 +90,7 @@ impl Client {
|
||||
tags: mongo_indexed::collection(&db, true).await?,
|
||||
variables: mongo_indexed::collection(&db, true).await?,
|
||||
git_accounts: mongo_indexed::collection(&db, true).await?,
|
||||
registry_accounts: mongo_indexed::collection(&db, true).await?,
|
||||
registry_accounts: registry_accounts_collection(&db).await?,
|
||||
updates: mongo_indexed::collection(&db, true).await?,
|
||||
alerts: mongo_indexed::collection(&db, true).await?,
|
||||
stats: mongo_indexed::collection(&db, true).await?,
|
||||
@@ -263,3 +263,39 @@ where
|
||||
bcrypt::hash(password, BCRYPT_COST)
|
||||
.context("failed to hash password")
|
||||
}
|
||||
|
||||
/// Prior to v2.3.0, the image registry accounts were stored in
|
||||
/// DockerRegistryAccount collection instead of default ImageRegistryAccount.
|
||||
/// This method selects the correct name to use for any Komodo database,
|
||||
/// including legacy installations, where the old name continues
|
||||
/// to be used.
|
||||
async fn registry_accounts_collection(
|
||||
db: &Database,
|
||||
) -> anyhow::Result<Collection<ImageRegistryAccount>> {
|
||||
let name = if db
|
||||
.collection::<Document>("ImageRegistryAccount")
|
||||
.count_documents(Document::new())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
> 0
|
||||
{
|
||||
// If ImageRegistryAccount collection is non-empty in any case, prefer it.
|
||||
"ImageRegistryAccount"
|
||||
} else if db
|
||||
.collection::<Document>("DockerRegistryAccount")
|
||||
.count_documents(Document::new())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
> 0
|
||||
{
|
||||
// If already using DockerRegistryAccount collection, prefer it instead.
|
||||
"DockerRegistryAccount"
|
||||
} else {
|
||||
// If using neither, prefer the new convention
|
||||
"ImageRegistryAccount"
|
||||
};
|
||||
|
||||
mongo_indexed::collection_with_name(db, name, true)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::http::HeaderValue;
|
||||
use axum::http::{self, HeaderValue};
|
||||
use bytes::Bytes;
|
||||
use encoding::CastBytes as _;
|
||||
use futures_util::{
|
||||
@@ -15,7 +15,8 @@ use tokio::net::TcpStream;
|
||||
use tokio_tungstenite::{
|
||||
Connector, MaybeTlsStream, WebSocketStream,
|
||||
tungstenite::{
|
||||
self, handshake::client::Response, protocol::CloseFrame,
|
||||
self, client::IntoClientRequest as _,
|
||||
handshake::client::Response, protocol::CloseFrame,
|
||||
},
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -183,7 +184,8 @@ impl TungsteniteWebsocket {
|
||||
pub async fn connect(
|
||||
url: &str,
|
||||
) -> mogh_error::Result<(Self, HeaderValue)> {
|
||||
let res = tokio_tungstenite::connect_async(url).await;
|
||||
let res =
|
||||
tokio_tungstenite::connect_async(make_request(url)?).await;
|
||||
Self::handle_connection_result(url, res)
|
||||
}
|
||||
|
||||
@@ -191,7 +193,7 @@ impl TungsteniteWebsocket {
|
||||
url: &str,
|
||||
) -> mogh_error::Result<(Self, HeaderValue)> {
|
||||
let res = tokio_tungstenite::connect_async_tls_with_config(
|
||||
url,
|
||||
make_request(url)?,
|
||||
None,
|
||||
false,
|
||||
Some(Connector::Rustls(Arc::new(
|
||||
@@ -239,6 +241,19 @@ impl TungsteniteWebsocket {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_request(url: &str) -> mogh_error::Result<http::Request<()>> {
|
||||
let mut request =
|
||||
url.into_client_request().context("Invalid websocket URL")?;
|
||||
request.headers_mut().insert(
|
||||
"user-agent",
|
||||
HeaderValue::from_static(concat!(
|
||||
"komodo/",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)),
|
||||
);
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InsecureVerifier;
|
||||
|
||||
|
||||
Vendored
+23
-5
@@ -369,8 +369,10 @@ export interface CheckDeploymentForUpdateResponse {
|
||||
export type BatchCheckDeploymentForUpdateResponse = CheckDeploymentForUpdateResponse[];
|
||||
export interface StackServiceWithUpdate {
|
||||
service: string;
|
||||
/** The service's image */
|
||||
/** The service's (current) image */
|
||||
image: string;
|
||||
/** The latest image (if different than current) */
|
||||
latest_image?: string;
|
||||
/** Whether there is a newer image available for this service */
|
||||
update_available: boolean;
|
||||
}
|
||||
@@ -2992,10 +2994,7 @@ export interface SystemInformation {
|
||||
kernel?: string;
|
||||
/** Physical core count */
|
||||
core_count?: number;
|
||||
/**
|
||||
* Logical core count. If available,
|
||||
* used to interpret system load accurately.
|
||||
*/
|
||||
/** Logical core count. */
|
||||
logical_core_count?: number;
|
||||
/** System hostname based off DNS */
|
||||
host_name?: string;
|
||||
@@ -5414,6 +5413,21 @@ export interface MinimalSystemStats {
|
||||
/** Unix timestamp in milliseconds when disk list was last refreshed */
|
||||
refresh_list_ts: I64;
|
||||
}
|
||||
/** Just the server alerting thresholds */
|
||||
export interface ServerAlertingThresholds {
|
||||
/** The percentage threshhold which triggers WARNING state for CPU. */
|
||||
cpu_warning: number;
|
||||
/** The percentage threshhold which triggers CRITICAL state for CPU. */
|
||||
cpu_critical: number;
|
||||
/** The percentage threshhold which triggers WARNING state for MEM. */
|
||||
mem_warning: number;
|
||||
/** The percentage threshhold which triggers CRITICAL state for MEM. */
|
||||
mem_critical: number;
|
||||
/** The percentage threshhold which triggers WARNING state for DISK. */
|
||||
disk_warning: number;
|
||||
/** The percentage threshhold which triggers CRITICAL state for DISK. */
|
||||
disk_critical: number;
|
||||
}
|
||||
export interface ServerListItemInfo {
|
||||
/** The server's state. */
|
||||
state: ServerState;
|
||||
@@ -5424,6 +5438,8 @@ export interface ServerListItemInfo {
|
||||
err?: _Serror;
|
||||
/** System stats, if available */
|
||||
stats?: MinimalSystemStats;
|
||||
/** The server alerting thresholds */
|
||||
alerting_thresholds: ServerAlertingThresholds;
|
||||
/** The server's number of physical cores. */
|
||||
core_count?: number;
|
||||
/** The server's number of logical cores. */
|
||||
@@ -5528,6 +5544,8 @@ export interface StackListItemInfo {
|
||||
* Otherwise, its `latest_services`
|
||||
*/
|
||||
services: StackServiceWithUpdate[];
|
||||
/** Whether stack has auto_update_all_services enabled. */
|
||||
auto_update_all_services: boolean;
|
||||
/**
|
||||
* Whether the compose project is missing on the host.
|
||||
* Ie, it does not show up in `docker compose ls`.
|
||||
|
||||
+20
-1
@@ -660,7 +660,12 @@ export function useFilterByUpdateAvailable(): [boolean, () => void] {
|
||||
|
||||
export function usePermissions({ type, id }: Types.ResourceTarget) {
|
||||
const user = useUser().data;
|
||||
const perms = useRead("GetPermission", { target: { type, id } }).data as
|
||||
const perms = useRead(
|
||||
"GetPermission",
|
||||
{ target: { type, id } },
|
||||
// skip call for admins
|
||||
{ enabled: user ? !user?.admin : false },
|
||||
).data as
|
||||
| Types.PermissionLevelAndSpecifics
|
||||
| Types.PermissionLevel
|
||||
| undefined;
|
||||
@@ -668,6 +673,20 @@ export function usePermissions({ type, id }: Types.ResourceTarget) {
|
||||
const ui_write_disabled = info?.ui_write_disabled ?? false;
|
||||
const disable_non_admin_create = info?.disable_non_admin_create ?? false;
|
||||
|
||||
if (user?.admin) {
|
||||
return {
|
||||
canWrite: true,
|
||||
canExecute: true,
|
||||
canCreate: true,
|
||||
specific: Object.values(Types.SpecificPermission),
|
||||
specificLogs: true,
|
||||
specificInspect: true,
|
||||
specificTerminal: true,
|
||||
specificAttach: true,
|
||||
specificProcesses: true,
|
||||
};
|
||||
}
|
||||
|
||||
const level =
|
||||
(perms && typeof perms === "string"
|
||||
? perms
|
||||
|
||||
@@ -4,6 +4,8 @@ import sanitizeHtml from "sanitize-html";
|
||||
import ConvertAnsiToHtml from "ansi-to-html";
|
||||
import { RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
export const EXECUTION_ACTION_STATE_REQUERY_MS = 500;
|
||||
|
||||
export function objectKeys<T extends object>(o: T): (keyof T)[] {
|
||||
return Object.keys(o) as (keyof T)[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Page } from "mogh_ui";
|
||||
import { useDashboardPreferences, useSetTitle } from "@/lib/hooks";
|
||||
import {
|
||||
useDashboardPreferences,
|
||||
useSetTitle,
|
||||
useTagsFilter,
|
||||
} from "@/lib/hooks";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import { Group } from "@mantine/core";
|
||||
import DashboardRecents from "./recents";
|
||||
@@ -11,6 +15,7 @@ import DashboardActiveResources from "./active";
|
||||
|
||||
export default function Dashboard() {
|
||||
const { preferences } = useDashboardPreferences();
|
||||
const tags = useTagsFilter();
|
||||
useSetTitle(undefined);
|
||||
return (
|
||||
<Page
|
||||
@@ -21,7 +26,7 @@ export default function Dashboard() {
|
||||
<Group w={{ base: "100%", xs: "fit-content" }}>
|
||||
<ShowTables />
|
||||
<ServerShowStats />
|
||||
<ExportToml />
|
||||
<ExportToml tags={preferences.showTables ? tags : undefined} />
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useResourceParamType, useSetTitle } from "@/lib/hooks";
|
||||
import { useResourceParamType, useSetTitle, useTagsFilter } from "@/lib/hooks";
|
||||
import { ResourceComponents, UsableResource } from "@/resources";
|
||||
import { Types } from "komodo_client";
|
||||
import { Page } from "mogh_ui";
|
||||
@@ -18,6 +18,8 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
|
||||
|
||||
const [query, setQuery] = useState<Types.ResourceQuery<any>>({});
|
||||
|
||||
const tags = useTagsFilter();
|
||||
|
||||
const RC = ResourceComponents[type];
|
||||
|
||||
if (!RC) {
|
||||
@@ -32,7 +34,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) {
|
||||
oppositeTitle={
|
||||
<Group w={{ base: "100%", xs: "fit-content" }}>
|
||||
{type === "Server" && <ServerShowStats />}
|
||||
<ExportToml listQuery={{ type, query }} />
|
||||
<ExportToml listQuery={{ type, query }} tags={tags} />
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
import { useExecute, useIsCancelling, useRead } from "@/lib/hooks";
|
||||
import {
|
||||
useExecute,
|
||||
useInvalidate,
|
||||
useIsCancelling,
|
||||
useRead,
|
||||
} from "@/lib/hooks";
|
||||
import { useAction } from ".";
|
||||
import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
import { Types } from "komodo_client";
|
||||
import { EXECUTION_ACTION_STATE_REQUERY_MS } from "@/lib/utils";
|
||||
|
||||
export function RunAction({ id }: { id: string }) {
|
||||
const invalidate = useInvalidate();
|
||||
const running =
|
||||
(useRead("GetActionActionState", { action: id }, { refetchInterval: 5_000 })
|
||||
.data?.running ?? 0) > 0;
|
||||
const { mutateAsync: run, isPending: runPending } = useExecute("RunAction");
|
||||
const { mutateAsync: cancel, isPending: cancelPending } =
|
||||
useExecute("CancelAction");
|
||||
(useRead(
|
||||
"GetActionActionState",
|
||||
{ action: id },
|
||||
{ refetchInterval: 5_000 },
|
||||
).data?.running ?? 0) > 0;
|
||||
const { mutateAsync: run, isPending: runPending } = useExecute("RunAction", {
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetActionActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
});
|
||||
const { mutateAsync: cancel, isPending: cancelPending } = useExecute(
|
||||
"CancelAction",
|
||||
{
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetActionActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
},
|
||||
);
|
||||
const action = useAction(id);
|
||||
const cancelling = useIsCancelling(
|
||||
{ type: "Action", id },
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
import { Types } from "komodo_client";
|
||||
import {
|
||||
useExecute,
|
||||
useInvalidate,
|
||||
useIsCancelling,
|
||||
usePermissions,
|
||||
useRead,
|
||||
} from "@/lib/hooks";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import { EXECUTION_ACTION_STATE_REQUERY_MS } from "@/lib/utils";
|
||||
|
||||
export function RunBuild({ id }: { id: string }) {
|
||||
const invalidate = useInvalidate();
|
||||
const { canExecute } = usePermissions({ type: "Build", id });
|
||||
const building = useRead(
|
||||
"GetBuildActionState",
|
||||
{ build: id },
|
||||
{ refetchInterval: 5_000 },
|
||||
).data?.building;
|
||||
const { mutate: run, isPending: runPending } = useExecute("RunBuild");
|
||||
const { mutate: cancel, isPending: cancelPending } =
|
||||
useExecute("CancelBuild");
|
||||
const { mutate: run, isPending: runPending } = useExecute("RunBuild", {
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetBuildActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
});
|
||||
const { mutate: cancel, isPending: cancelPending } = useExecute(
|
||||
"CancelBuild",
|
||||
{
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetBuildActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
},
|
||||
);
|
||||
const cancelling = useIsCancelling(
|
||||
{ type: "Build", id },
|
||||
Types.Operation.RunBuild,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Group, Select, Stack, Text } from "@mantine/core";
|
||||
import { useExecute, useListItem, useRead } from "@/lib/hooks";
|
||||
import { useExecute, useInvalidate, useListItem, useRead } from "@/lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { parseKeyValue } from "@/lib/utils";
|
||||
import { EXECUTION_ACTION_STATE_REQUERY_MS, parseKeyValue } from "@/lib/utils";
|
||||
import { useDeployment } from ".";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
@@ -12,6 +12,17 @@ interface DeploymentId {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const useInvalidateActionState = () => {
|
||||
const invalidate = useInvalidate();
|
||||
return {
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetDeploymentActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export function DeployDeployment({ id }: DeploymentId) {
|
||||
const deployment = useRead("GetDeployment", { deployment: id }).data;
|
||||
const [signal, setSignal] = useState<Types.TerminationSignal>();
|
||||
@@ -21,7 +32,10 @@ export function DeployDeployment({ id }: DeploymentId) {
|
||||
[deployment?.config?.termination_signal],
|
||||
);
|
||||
|
||||
const { mutateAsync: deploy, isPending } = useExecute("Deploy");
|
||||
const { mutateAsync: deploy, isPending } = useExecute(
|
||||
"Deploy",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
|
||||
const deployment_item = useListItem("Deployment", id);
|
||||
|
||||
@@ -90,7 +104,10 @@ export function DestroyDeployment({ id }: DeploymentId) {
|
||||
[deployment?.config?.termination_signal],
|
||||
);
|
||||
|
||||
const { mutateAsync: destroy, isPending } = useExecute("DestroyDeployment");
|
||||
const { mutateAsync: destroy, isPending } = useExecute(
|
||||
"DestroyDeployment",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
|
||||
const state = useListItem("Deployment", id)?.info.state;
|
||||
|
||||
@@ -137,7 +154,10 @@ export function DestroyDeployment({ id }: DeploymentId) {
|
||||
|
||||
export function PullDeployment({ id }: DeploymentId) {
|
||||
const deployment = useDeployment(id);
|
||||
const { mutate: pull, isPending: pullPending } = useExecute("PullDeployment");
|
||||
const { mutate: pull, isPending: pullPending } = useExecute(
|
||||
"PullDeployment",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const action_state = useRead(
|
||||
"GetDeploymentActionState",
|
||||
{
|
||||
@@ -163,8 +183,10 @@ export function PullDeployment({ id }: DeploymentId) {
|
||||
export function RestartDeployment({ id }: DeploymentId) {
|
||||
const deployment = useDeployment(id);
|
||||
const state = deployment?.info.state;
|
||||
const { mutateAsync: restart, isPending: restartPending } =
|
||||
useExecute("RestartDeployment");
|
||||
const { mutateAsync: restart, isPending: restartPending } = useExecute(
|
||||
"RestartDeployment",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const action_state = useRead(
|
||||
"GetDeploymentActionState",
|
||||
{
|
||||
@@ -195,8 +217,10 @@ export function RestartDeployment({ id }: DeploymentId) {
|
||||
export function StartStopDeployment({ id }: DeploymentId) {
|
||||
const deployment = useDeployment(id);
|
||||
const state = deployment?.info.state;
|
||||
const { mutate: start, isPending: startPending } =
|
||||
useExecute("StartDeployment");
|
||||
const { mutate: start, isPending: startPending } = useExecute(
|
||||
"StartDeployment",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const action_state = useRead(
|
||||
"GetDeploymentActionState",
|
||||
{
|
||||
@@ -233,7 +257,10 @@ function StopDeployment({ id }: DeploymentId) {
|
||||
[deployment?.config?.termination_signal],
|
||||
);
|
||||
|
||||
const { mutateAsync: stop, isPending } = useExecute("StopDeployment");
|
||||
const { mutateAsync: stop, isPending } = useExecute(
|
||||
"StopDeployment",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const stopping = useRead(
|
||||
"GetDeploymentActionState",
|
||||
{
|
||||
@@ -306,10 +333,14 @@ function TermSignalSelector({
|
||||
export function PauseUnpauseDeployment({ id }: DeploymentId) {
|
||||
const deployment = useDeployment(id);
|
||||
const state = deployment?.info.state;
|
||||
const { mutate: unpause, isPending: unpausePending } =
|
||||
useExecute("UnpauseDeployment");
|
||||
const { mutateAsync: pause, isPending: pausePending } =
|
||||
useExecute("PauseDeployment");
|
||||
const { mutate: unpause, isPending: unpausePending } = useExecute(
|
||||
"UnpauseDeployment",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const { mutateAsync: pause, isPending: pausePending } = useExecute(
|
||||
"PauseDeployment",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const action_state = useRead(
|
||||
"GetDeploymentActionState",
|
||||
{
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
import { useExecute, useIsCancelling, useRead } from "@/lib/hooks";
|
||||
import {
|
||||
useExecute,
|
||||
useInvalidate,
|
||||
useIsCancelling,
|
||||
useRead,
|
||||
} from "@/lib/hooks";
|
||||
import { useProcedure } from ".";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
import { Types } from "komodo_client";
|
||||
import { EXECUTION_ACTION_STATE_REQUERY_MS } from "@/lib/utils";
|
||||
|
||||
export function RunProcedure({ id }: { id: string }) {
|
||||
const invalidate = useInvalidate();
|
||||
const running = useRead(
|
||||
"GetProcedureActionState",
|
||||
{ procedure: id },
|
||||
{ refetchInterval: 5_000 },
|
||||
).data?.running;
|
||||
const { mutateAsync: run, isPending: runPending } =
|
||||
useExecute("RunProcedure");
|
||||
const { mutateAsync: cancel, isPending: cancelPending } =
|
||||
useExecute("CancelProcedure");
|
||||
const { mutateAsync: run, isPending: runPending } = useExecute(
|
||||
"RunProcedure",
|
||||
{
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetProcedureActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
},
|
||||
);
|
||||
const { mutateAsync: cancel, isPending: cancelPending } = useExecute(
|
||||
"CancelProcedure",
|
||||
{
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetProcedureActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
},
|
||||
);
|
||||
const procedure = useProcedure(id);
|
||||
const cancelling = useIsCancelling(
|
||||
{ type: "Procedure", id },
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
import { useExecute, usePermissions, useRead } from "@/lib/hooks";
|
||||
import {
|
||||
useExecute,
|
||||
useInvalidate,
|
||||
usePermissions,
|
||||
useRead,
|
||||
} from "@/lib/hooks";
|
||||
import { useRepo } from ".";
|
||||
import { useBuilder } from "../builder";
|
||||
import { Types } from "komodo_client";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import { EXECUTION_ACTION_STATE_REQUERY_MS } from "@/lib/utils";
|
||||
|
||||
const useInvalidateActionState = () => {
|
||||
const invalidate = useInvalidate();
|
||||
return {
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetRepoActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export function CloneRepo({ id }: { id: string }) {
|
||||
const { mutate, isPending } = useExecute("CloneRepo");
|
||||
const { mutate, isPending } = useExecute(
|
||||
"CloneRepo",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const cloning = useRead(
|
||||
"GetRepoActionState",
|
||||
{ repo: id },
|
||||
@@ -30,7 +50,10 @@ export function CloneRepo({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
export function PullRepo({ id }: { id: string }) {
|
||||
const { mutate, isPending } = useExecute("PullRepo");
|
||||
const { mutate, isPending } = useExecute(
|
||||
"PullRepo",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const pulling = useRead(
|
||||
"GetRepoActionState",
|
||||
{ repo: id },
|
||||
@@ -67,9 +90,14 @@ export function BuildRepo({ id }: { id: string }) {
|
||||
"target.id": id,
|
||||
},
|
||||
}).data;
|
||||
const { mutate: run_mutate, isPending: runPending } = useExecute("BuildRepo");
|
||||
const { mutate: cancel_mutate, isPending: cancelPending } =
|
||||
useExecute("CancelRepoBuild");
|
||||
const { mutate: run_mutate, isPending: runPending } = useExecute(
|
||||
"BuildRepo",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const { mutate: cancel_mutate, isPending: cancelPending } = useExecute(
|
||||
"CancelRepoBuild",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
|
||||
const repo = useRepo(id);
|
||||
const builder = useBuilder(repo?.info.builder_id);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { fmtResourceType } from "@/lib/formatting";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import { useDebounce, useSearchCombobox } from "mogh_ui";
|
||||
import { keepPreviousData } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useRead } from "@/lib/hooks";
|
||||
|
||||
export interface ResourceSelectorProps extends ComboboxProps {
|
||||
@@ -84,6 +84,14 @@ export default function ResourceSelector({
|
||||
[__resources],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// useSearchCombobox only selects the first option when the search
|
||||
// input changes, but the options settle later (debounce + fetch),
|
||||
// which can leave nothing selected. Select the first option
|
||||
// whenever newly settled options render.
|
||||
combobox.selectFirstOption();
|
||||
}, [resources]);
|
||||
|
||||
const name = selectedResource?.name;
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { useExecute, usePermissions, useRead } from "@/lib/hooks";
|
||||
import {
|
||||
useExecute,
|
||||
useInvalidate,
|
||||
usePermissions,
|
||||
useRead,
|
||||
} from "@/lib/hooks";
|
||||
import { useServer } from ".";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable";
|
||||
import { EXECUTION_ACTION_STATE_REQUERY_MS } from "@/lib/utils";
|
||||
|
||||
export const Prune = ({
|
||||
serverId,
|
||||
@@ -12,7 +18,14 @@ export const Prune = ({
|
||||
type: "Containers" | "Networks" | "Images" | "Volumes" | "Buildx" | "System";
|
||||
}) => {
|
||||
const server = useServer(serverId);
|
||||
const { mutateAsync: prune, isPending } = useExecute(`Prune${type}`);
|
||||
const invalidate = useInvalidate();
|
||||
const { mutateAsync: prune, isPending } = useExecute(`Prune${type}`, {
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetServerActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
});
|
||||
const action_state = useRead(
|
||||
"GetServerActionState",
|
||||
{ server: serverId },
|
||||
|
||||
@@ -58,21 +58,19 @@ export function useServerStats(id: string) {
|
||||
).data;
|
||||
}
|
||||
|
||||
export function useServerThresholds(id: string) {
|
||||
const isServerAvailable = useIsServerAvailable(id);
|
||||
const config = useRead(
|
||||
"GetServer",
|
||||
{ server: id },
|
||||
{
|
||||
enabled: isServerAvailable,
|
||||
},
|
||||
).data?.config as any;
|
||||
export function serverThresholds(server: Types.ServerListItem | undefined) {
|
||||
const thresholds = server?.info.alerting_thresholds;
|
||||
return {
|
||||
cpuWarning: config?.cpu_warning ?? 75,
|
||||
cpuCritical: config?.cpu_critical ?? 90,
|
||||
memWarning: config?.mem_warning ?? 75,
|
||||
memCritical: config?.mem_critical ?? 90,
|
||||
diskWarning: config?.disk_warning ?? 75,
|
||||
diskCritical: config?.disk_critical ?? 90,
|
||||
cpuWarning: thresholds?.cpu_warning ?? 75,
|
||||
cpuCritical: thresholds?.cpu_critical ?? 90,
|
||||
memWarning: thresholds?.mem_warning ?? 75,
|
||||
memCritical: thresholds?.mem_critical ?? 90,
|
||||
diskWarning: thresholds?.disk_warning ?? 75,
|
||||
diskCritical: thresholds?.disk_critical ?? 90,
|
||||
};
|
||||
}
|
||||
|
||||
export function useServerThresholds(id: string) {
|
||||
const server = useServer(id);
|
||||
return serverThresholds(server);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { ColorIntention, hexColorByIntention } from "mogh_ui";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { serverThresholds } from "./hooks";
|
||||
|
||||
export interface ServerStatsCardProps {
|
||||
id: string;
|
||||
@@ -25,20 +26,14 @@ export default function ServerStatsCard({ id }: ServerStatsCardProps) {
|
||||
const isServerAvailable = server?.info.state === Types.ServerState.Ok;
|
||||
const enabled = preferences.showServerStats && isServerAvailable;
|
||||
|
||||
const serverDetails = useRead(
|
||||
"GetServer",
|
||||
{ server: id },
|
||||
{
|
||||
enabled,
|
||||
},
|
||||
).data;
|
||||
|
||||
const cpuWarning = serverDetails?.config?.cpu_warning ?? 75;
|
||||
const cpuCritical = serverDetails?.config?.cpu_critical ?? 90;
|
||||
const memWarning = serverDetails?.config?.mem_warning ?? 75;
|
||||
const memCritical = serverDetails?.config?.mem_critical ?? 90;
|
||||
const diskWarning = serverDetails?.config?.disk_warning ?? 75;
|
||||
const diskCritical = serverDetails?.config?.disk_critical ?? 90;
|
||||
const {
|
||||
cpuWarning,
|
||||
cpuCritical,
|
||||
memWarning,
|
||||
memCritical,
|
||||
diskWarning,
|
||||
diskCritical,
|
||||
} = serverThresholds(server);
|
||||
|
||||
const intention = (percentage: number, type: "cpu" | "memory" | "disk") => {
|
||||
const warning =
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { useExecute, useRead } from "@/lib/hooks";
|
||||
import { useExecute, useInvalidate, useRead } from "@/lib/hooks";
|
||||
import { Types } from "komodo_client";
|
||||
import { useStack } from ".";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
import { ICONS } from "@/lib/icons";
|
||||
import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable";
|
||||
import { EXECUTION_ACTION_STATE_REQUERY_MS } from "@/lib/utils";
|
||||
|
||||
const useInvalidateActionState = () => {
|
||||
const invalidate = useInvalidate();
|
||||
return {
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetStackActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const DeployStack = ({
|
||||
id,
|
||||
@@ -14,7 +26,10 @@ export const DeployStack = ({
|
||||
}) => {
|
||||
const stack = useStack(id);
|
||||
const state = stack?.info.state;
|
||||
const { mutateAsync: deploy, isPending } = useExecute("DeployStack");
|
||||
const { mutateAsync: deploy, isPending } = useExecute(
|
||||
"DeployStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const deploying = useRead(
|
||||
"GetStackActionState",
|
||||
{ stack: id },
|
||||
@@ -78,7 +93,10 @@ export const DestroyStack = ({
|
||||
}) => {
|
||||
const stack = useStack(id);
|
||||
const state = stack?.info.state;
|
||||
const { mutateAsync: destroy, isPending } = useExecute("DestroyStack");
|
||||
const { mutateAsync: destroy, isPending } = useExecute(
|
||||
"DestroyStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const destroying = useRead(
|
||||
"GetStackActionState",
|
||||
{ stack: id },
|
||||
@@ -122,7 +140,10 @@ export const PullStack = ({
|
||||
service?: string;
|
||||
}) => {
|
||||
const stack = useStack(id);
|
||||
const { mutate: pull, isPending: pullPending } = useExecute("PullStack");
|
||||
const { mutate: pull, isPending: pullPending } = useExecute(
|
||||
"PullStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const actionState = useRead(
|
||||
"GetStackActionState",
|
||||
{ stack: id },
|
||||
@@ -158,8 +179,10 @@ export const RestartStack = ({
|
||||
}) => {
|
||||
const stack = useStack(id);
|
||||
const state = stack?.info.state;
|
||||
const { mutateAsync: restart, isPending: restartPending } =
|
||||
useExecute("RestartStack");
|
||||
const { mutateAsync: restart, isPending: restartPending } = useExecute(
|
||||
"RestartStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const actionState = useRead(
|
||||
"GetStackActionState",
|
||||
{ stack: id },
|
||||
@@ -206,8 +229,14 @@ export const StartStopStack = ({
|
||||
}) => {
|
||||
const stack = useStack(id);
|
||||
const state = stack?.info.state ?? Types.StackState.Unknown;
|
||||
const { mutate: start, isPending: startPending } = useExecute("StartStack");
|
||||
const { mutateAsync: stop, isPending: stopPending } = useExecute("StopStack");
|
||||
const { mutate: start, isPending: startPending } = useExecute(
|
||||
"StartStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const { mutateAsync: stop, isPending: stopPending } = useExecute(
|
||||
"StopStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const actionState = useRead(
|
||||
"GetStackActionState",
|
||||
{ stack: id },
|
||||
@@ -278,10 +307,14 @@ export const PauseUnpauseStack = ({
|
||||
}) => {
|
||||
const stack = useStack(id);
|
||||
const state = stack?.info.state;
|
||||
const { mutate: unpause, isPending: unpausePending } =
|
||||
useExecute("UnpauseStack");
|
||||
const { mutateAsync: pause, isPending: pausePending } =
|
||||
useExecute("PauseStack");
|
||||
const { mutate: unpause, isPending: unpausePending } = useExecute(
|
||||
"UnpauseStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const { mutateAsync: pause, isPending: pausePending } = useExecute(
|
||||
"PauseStack",
|
||||
useInvalidateActionState(),
|
||||
);
|
||||
const actionState = useRead(
|
||||
"GetStackActionState",
|
||||
{ stack: id },
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useWrite,
|
||||
} from "@/lib/hooks";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useFullStack, useStack } from ".";
|
||||
import { useStack } from ".";
|
||||
import { Types } from "komodo_client";
|
||||
import {
|
||||
ActionIcon,
|
||||
@@ -49,7 +49,6 @@ export default function StackUpdateAvailable({
|
||||
const pending = isPending || deploying;
|
||||
|
||||
const stack = useStack(id);
|
||||
const fullStack = useFullStack(id);
|
||||
const info = stack?.info;
|
||||
const state = info?.state ?? Types.StackState.Unknown;
|
||||
|
||||
@@ -98,10 +97,7 @@ export default function StackUpdateAvailable({
|
||||
)}
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<Services
|
||||
services={info?.services}
|
||||
latestServices={fullStack?.info?.latest_services}
|
||||
/>
|
||||
<Services services={info?.services} />
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
</Box>
|
||||
@@ -127,23 +123,20 @@ export default function StackUpdateAvailable({
|
||||
onConfirm={() =>
|
||||
deploy({
|
||||
stack: id,
|
||||
services: fullStack?.config?.auto_update_all_services
|
||||
services: info?.auto_update_all_services
|
||||
? []
|
||||
: servicesWithUpdate.map((s) => s.service),
|
||||
})
|
||||
}
|
||||
loading={pending}
|
||||
topAdditonal={
|
||||
!fullStack?.config?.auto_update_all_services && (
|
||||
!info?.auto_update_all_services && (
|
||||
<Stack className="bordered-light" p="md" bdrs="md" gap="sm">
|
||||
<Text size="lg">
|
||||
Service
|
||||
{servicesWithUpdate.length === 1 ? "" : "s"} with update:
|
||||
</Text>
|
||||
<Services
|
||||
services={info?.services}
|
||||
latestServices={fullStack?.info?.latest_services}
|
||||
/>
|
||||
<Services services={info?.services} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -170,10 +163,8 @@ export default function StackUpdateAvailable({
|
||||
|
||||
function Services({
|
||||
services,
|
||||
latestServices,
|
||||
}: {
|
||||
services: Types.StackServiceWithUpdate[] | undefined;
|
||||
latestServices: Types.StackServiceNames[] | undefined;
|
||||
}) {
|
||||
return (
|
||||
<Stack gap="0">
|
||||
@@ -183,10 +174,7 @@ function Services({
|
||||
<Group key={s.service} gap="xs">
|
||||
<Text c="dimmed">{s.service}</Text>
|
||||
<Text c="dimmed"> - </Text>
|
||||
<Text>
|
||||
{latestServices?.find((ser) => ser.service_name == s.service)
|
||||
?.image ?? s.image}
|
||||
</Text>
|
||||
<Text>{s.latest_image || s.image}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@@ -10,7 +10,11 @@ import { ICONS } from "@/lib/icons";
|
||||
import { ConfirmButton } from "mogh_ui";
|
||||
import { useFullResourceSync } from ".";
|
||||
import { useResourceSyncTabsView } from "./hooks";
|
||||
import { fileContentsEmpty, resourceSyncNoChanges } from "@/lib/utils";
|
||||
import {
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
fileContentsEmpty,
|
||||
resourceSyncNoChanges,
|
||||
} from "@/lib/utils";
|
||||
import ConfirmModalWithDisable from "@/components/confirm-modal-with-disable";
|
||||
|
||||
export function RefreshSync({ id }: { id: string }) {
|
||||
@@ -39,7 +43,14 @@ export function RefreshSync({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
export function ExecuteSync({ id }: { id: string }) {
|
||||
const { mutateAsync: execute, isPending } = useExecute("RunSync");
|
||||
const invalidate = useInvalidate();
|
||||
const { mutateAsync: execute, isPending } = useExecute("RunSync", {
|
||||
onSuccess: () =>
|
||||
setTimeout(
|
||||
() => invalidate(["GetResourceSyncActionState"]),
|
||||
EXECUTION_ACTION_STATE_REQUERY_MS,
|
||||
),
|
||||
});
|
||||
const syncing = useRead(
|
||||
"GetResourceSyncActionState",
|
||||
{ sync: id },
|
||||
|
||||
Reference in New Issue
Block a user