Compare commits

...
1 Commits
Author SHA1 Message Date
Maxwell BeckerandGitHub 93cce3fb60 2.3.1 (#1553)
* add static connection user agent

* include server alerting thresholds in list info to reduce query

* fix database registry accounts lost

* dev-1

* deploy 2.3.1-dev-2

* CoreReport -> KomodoReport

* stack update available avoid many GetStack call on tables

* deploy 2.3.1-dev-3

* dashboard tables toml respects tag filter

* add post execution invalidate for better feedback when ws update is missed

* resource selector select first result on settle

* skip GetPermission call for admin users

* fmt

* 2.3.1

* fix clippy lint

* ts 2.3.1
2026-07-31 14:01:30 -07:00
33 changed files with 549 additions and 169 deletions
Generated
+14 -14
View File
@@ -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
View File
@@ -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"
+35 -1
View File
@@ -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 {
+2 -2
View File
@@ -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,
};
+1
View File
@@ -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),
+27 -19
View File
@@ -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 != &current_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,
},
}
}
+1 -1
View File
@@ -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();
}
+2 -2
View File
@@ -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
+45
View File
@@ -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)]
+5 -1
View File
@@ -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 -1
View File
@@ -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",
+24 -5
View File
@@ -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
View File
@@ -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)
}
+19 -4
View File
@@ -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;
+23 -5
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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)[];
}
+7 -2
View File
@@ -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>
}
>
+4 -2
View File
@@ -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>
}
>
+30 -6
View File
@@ -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 },
+20 -3
View File
@@ -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,
+45 -14
View File
@@ -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",
{
+28 -5
View File
@@ -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 },
+34 -6
View File
@@ -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);
+9 -1
View File
@@ -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 (
+15 -2
View File
@@ -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 },
+13 -15
View File
@@ -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);
}
+9 -14
View File
@@ -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 =
+45 -12
View File
@@ -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 -18
View File
@@ -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>
+13 -2
View File
@@ -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 },