diff --git a/Cargo.lock b/Cargo.lock index e311975b40..8a65718dfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 8a5ed4e286..953ab28ac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/bin/core/src/api/write/stack.rs b/bin/core/src/api/write/stack.rs index 78e0b3cfe6..275efc72ce 100644 --- a/bin/core/src/api/write/stack.rs +++ b/bin/core/src/api/write/stack.rs @@ -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 { + latest_services.iter().find_map(|latest| { + if latest.service_name == service_name + && latest.image != current_image + { + Some(latest.image.clone()) + } else { + None + } + }) +} + // impl Resolve for BatchCheckStackForUpdate { diff --git a/bin/core/src/main.rs b/bin/core/src/main.rs index e96e277501..78afed7358 100644 --- a/bin/core/src/main.rs +++ b/bin/core/src/main.rs @@ -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; diff --git a/bin/core/src/reporting.rs b/bin/core/src/report.rs similarity index 95% rename from bin/core/src/reporting.rs rename to bin/core/src/report.rs index d10eadd9ca..6bcce720a2 100644 --- a/bin/core/src/reporting.rs +++ b/bin/core/src/report.rs @@ -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, }; diff --git a/bin/core/src/resource/server.rs b/bin/core/src/resource/server.rs index 1613a53a94..8c90f26672 100644 --- a/bin/core/src/resource/server.rs +++ b/bin/core/src/resource/server.rs @@ -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), diff --git a/bin/core/src/resource/stack.rs b/bin/core/src/resource/stack.rs index 2f7208fd1a..6fe8f4bde7 100644 --- a/bin/core/src/resource/stack.rs +++ b/bin/core/src/resource/stack.rs @@ -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, }, } } diff --git a/bin/periphery/src/stats/mod.rs b/bin/periphery/src/stats/mod.rs index eb70dafb00..1eb645cc09 100644 --- a/bin/periphery/src/stats/mod.rs +++ b/bin/periphery/src/stats/mod.rs @@ -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(); } diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index 012ece8000..ec1a7792e4 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -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] diff --git a/client/core/rs/src/entities/core_report.rs b/client/core/rs/src/entities/report.rs similarity index 60% rename from client/core/rs/src/entities/core_report.rs rename to client/core/rs/src/entities/report.rs index b3db82fde7..73f4ea03c6 100644 --- a/client/core/rs/src/entities/core_report.rs +++ b/client/core/rs/src/entities/report.rs @@ -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 diff --git a/client/core/rs/src/entities/server.rs b/client/core/rs/src/entities/server.rs index c32907c48d..30f9fc0812 100644 --- a/client/core/rs/src/entities/server.rs +++ b/client/core/rs/src/entities/server.rs @@ -44,6 +44,8 @@ pub struct ServerListItemInfo { pub err: Option<_Serror>, /// System stats, if available pub stats: Option, + /// The server alerting thresholds + pub alerting_thresholds: ServerAlertingThresholds, /// The server's number of physical cores. pub core_count: Option, /// 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)] diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index f77a07f475..912469e82a 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -184,6 +184,8 @@ pub struct StackListItemInfo { /// If deployed, will be `deployed_services`. /// Otherwise, its `latest_services` pub services: Vec, + /// 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, /// Whether there is a newer image available for this service pub update_available: bool, } diff --git a/client/core/ts/package.json b/client/core/ts/package.json index 061356db56..96d1346162 100644 --- a/client/core/ts/package.json +++ b/client/core/ts/package.json @@ -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", diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index a3ccdb6229..f17660f498 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -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`. diff --git a/lib/database/src/lib.rs b/lib/database/src/lib.rs index d0e50e1d2c..11863f437e 100644 --- a/lib/database/src/lib.rs +++ b/lib/database/src/lib.rs @@ -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> { + let name = if db + .collection::("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::("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) +} diff --git a/lib/transport/src/websocket/tungstenite.rs b/lib/transport/src/websocket/tungstenite.rs index f8a2ffef27..4a319d702f 100644 --- a/lib/transport/src/websocket/tungstenite.rs +++ b/lib/transport/src/websocket/tungstenite.rs @@ -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> { + 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; diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index b5abe89bab..b95fdd8e72 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -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`. diff --git a/ui/src/lib/hooks.ts b/ui/src/lib/hooks.ts index 7cfd546bc6..6f91b51954 100644 --- a/ui/src/lib/hooks.ts +++ b/ui/src/lib/hooks.ts @@ -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 diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts index e5229120ed..fd8e1fb2d9 100644 --- a/ui/src/lib/utils.ts +++ b/ui/src/lib/utils.ts @@ -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(o: T): (keyof T)[] { return Object.keys(o) as (keyof T)[]; } diff --git a/ui/src/pages/dashboard/index.tsx b/ui/src/pages/dashboard/index.tsx index b0d3ee3d7e..73ec876a9c 100644 --- a/ui/src/pages/dashboard/index.tsx +++ b/ui/src/pages/dashboard/index.tsx @@ -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 ( - + } > diff --git a/ui/src/pages/resources.tsx b/ui/src/pages/resources.tsx index 5c9e5144e0..a73e045426 100644 --- a/ui/src/pages/resources.tsx +++ b/ui/src/pages/resources.tsx @@ -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>({}); + const tags = useTagsFilter(); + const RC = ResourceComponents[type]; if (!RC) { @@ -32,7 +34,7 @@ export default function Resources({ _type }: { _type?: UsableResource }) { oppositeTitle={ {type === "Server" && } - + } > diff --git a/ui/src/resources/action/executions.tsx b/ui/src/resources/action/executions.tsx index 11c1669140..6983009d90 100644 --- a/ui/src/resources/action/executions.tsx +++ b/ui/src/resources/action/executions.tsx @@ -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 }, diff --git a/ui/src/resources/build/executions.tsx b/ui/src/resources/build/executions.tsx index ae62513937..410be9e933 100644 --- a/ui/src/resources/build/executions.tsx +++ b/ui/src/resources/build/executions.tsx @@ -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, diff --git a/ui/src/resources/deployment/executions.tsx b/ui/src/resources/deployment/executions.tsx index ff7495eccf..af2df71ba6 100644 --- a/ui/src/resources/deployment/executions.tsx +++ b/ui/src/resources/deployment/executions.tsx @@ -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(); @@ -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", { diff --git a/ui/src/resources/procedure/executions.tsx b/ui/src/resources/procedure/executions.tsx index 8d0a5b05a6..9a91eeea53 100644 --- a/ui/src/resources/procedure/executions.tsx +++ b/ui/src/resources/procedure/executions.tsx @@ -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 }, diff --git a/ui/src/resources/repo/executions.tsx b/ui/src/resources/repo/executions.tsx index 741df6bceb..8d46348eaa 100644 --- a/ui/src/resources/repo/executions.tsx +++ b/ui/src/resources/repo/executions.tsx @@ -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); diff --git a/ui/src/resources/selector.tsx b/ui/src/resources/selector.tsx index f589cc3057..52d57db6b5 100644 --- a/ui/src/resources/selector.tsx +++ b/ui/src/resources/selector.tsx @@ -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 ( diff --git a/ui/src/resources/server/executions.tsx b/ui/src/resources/server/executions.tsx index d91bae6f0a..34adad33bf 100644 --- a/ui/src/resources/server/executions.tsx +++ b/ui/src/resources/server/executions.tsx @@ -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 }, diff --git a/ui/src/resources/server/hooks.ts b/ui/src/resources/server/hooks.ts index 388669e921..1c6ea34118 100644 --- a/ui/src/resources/server/hooks.ts +++ b/ui/src/resources/server/hooks.ts @@ -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); +} diff --git a/ui/src/resources/server/stats-card.tsx b/ui/src/resources/server/stats-card.tsx index 23c1180509..8f0b35c19a 100644 --- a/ui/src/resources/server/stats-card.tsx +++ b/ui/src/resources/server/stats-card.tsx @@ -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 = diff --git a/ui/src/resources/stack/executions.tsx b/ui/src/resources/stack/executions.tsx index 78369f9750..883677a256 100644 --- a/ui/src/resources/stack/executions.tsx +++ b/ui/src/resources/stack/executions.tsx @@ -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 }, diff --git a/ui/src/resources/stack/update-available.tsx b/ui/src/resources/stack/update-available.tsx index bec48c1147..b574225754 100644 --- a/ui/src/resources/stack/update-available.tsx +++ b/ui/src/resources/stack/update-available.tsx @@ -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({ )} - + @@ -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 && ( Service {servicesWithUpdate.length === 1 ? "" : "s"} with update: - + ) } @@ -170,10 +163,8 @@ export default function StackUpdateAvailable({ function Services({ services, - latestServices, }: { services: Types.StackServiceWithUpdate[] | undefined; - latestServices: Types.StackServiceNames[] | undefined; }) { return ( @@ -183,10 +174,7 @@ function Services({ {s.service} - - - {latestServices?.find((ser) => ser.service_name == s.service) - ?.image ?? s.image} - + {s.latest_image || s.image} ))} diff --git a/ui/src/resources/sync/executions.tsx b/ui/src/resources/sync/executions.tsx index aeb6c935b6..63603f5528 100644 --- a/ui/src/resources/sync/executions.tsx +++ b/ui/src/resources/sync/executions.tsx @@ -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 },