Compare commits

..
2 Commits
Author SHA1 Message Date
Maxwell BeckerandGitHub 780ac68b99 2.3.3 (#1605)
* 2.4.0-dev-0

* deploy 2.4.0-dev-1

* slightly optimize stack read path

* Add IMAGE_NAME and IMAGE_TAG default build args in addition to VERSION

* deploy 2.4.0-dev-2

* ui use || location.origin for base url

* move to build time versioning

* simplify run action with komodo managed version

* procedure batch executions use  tag filters

* set advanced code splitting options

* improve network module logic / edge cases

* fix startup ordering - apply legacy db fixes before other tasks

* lengthen periphery command timeouts

* update deps

* clients still have explicit versions

* Add page breadcrumbs

* mogh ui 1.2.6

* fix set version – set only specific path

* cargo fmt

* use mogh ui ListPagination

* bump deps, rust 1.98.0

* missing dockerfile rust 1.98.0 bump
2026-09-01 14:34:49 -07:00
Maxwell BeckerandGitHub cc1c5aa5da 2.3.2 (#1578)
* UI Tags: remove owner column

* 2.3.2

* reporting error includes api message

* fix server builder config deserialization issue when server_ids not specified (causing UI failure)

* deploy 2.3.2-dev-1

* bump rs / ui dependencies. Breaking fixes: clap (rust), react table (ts), recharts (ts)

* improve ui index file from 4.7mb to 1.4mb with proper code splitting

* mogh ui 1.0.1 lock

* deploy 2.3.2-dev-2

* fix monaco yaml web worker for compose file suggestions

* small fix various console logs

* deploy 2.3.2-dev-3

* docsite configurable DOCSITE_URL and DOCSITE_BASE_URL

* align dockerfile with base url changes

* fix docsite docker caddy

* 2.3.2

* remove currently unsupported configuration (github webhook app)

* bump rust deps again

* bump react table
2026-08-11 13:58:11 -07:00
87 changed files with 1549 additions and 2110 deletions
Generated
+233 -918
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "2.3.1"
version = "0.0.0"
edition = "2024"
license = "GPL-3.0-or-later"
repository = "https://github.com/moghtech/komodo"
@@ -34,14 +34,14 @@ mogh_error = { version = "1.0.4", default-features = false }
derive_default_builder = "0.1.8"
async_timing_util = "1.1.0"
mogh_auth_client = "1.7.0"
mogh_auth_server = "1.7.0"
mogh_auth_server = "1.8.0"
mogh_secret_file = "1.0.1"
mogh_validations = "1.0.1"
mogh_rate_limit = "1.0.1"
partial_derive2 = "0.5.0"
mongo_indexed = "2.1.0"
mogh_resolver = "1.0.0"
mogh_config = "1.1.0"
mogh_config = "1.4.2"
mogh_logger = "1.3.4"
mogh_server = "1.5.0"
toml_pretty = "2.0.0"
@@ -56,7 +56,7 @@ tokio = { version = "1.53.1", features = ["full"] }
tokio-util = { version = "0.7.19", features = ["io", "codec"] }
tokio-stream = { version = "0.1.19", features = ["sync"] }
pin-project-lite = "0.2.17"
futures-util = "0.3.33"
futures-util = "0.3.34"
arc-swap = "1.9.2"
# SERVER
@@ -74,15 +74,15 @@ indexmap = { version = "2.14.0", features = ["serde"] }
serde = { version = "1.0.229", features = ["derive"] }
strum = { version = "0.28.0", features = ["derive"] }
bson = { version = "2.15.0" } # must keep in sync with mongodb version
toml = "1.1.3"
toml = "1.1.4"
serde_yaml_ng = "0.10.0"
serde_json = "1.0.151"
serde_qs = "1.1.2"
serde_qs = "1.1.3"
url = "2.5.8"
# ERROR
anyhow = "1.0.104"
thiserror = "2.0.19"
thiserror = "2.0.20"
# SYSTEM
libc = "0.2.189"
@@ -91,16 +91,16 @@ libc = "0.2.189"
tracing = "0.1.44"
# CONFIG
clap = { version = "4.6.4", features = ["derive"] }
clap = { version = "4.6.6", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.24.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.42", features = ["aws-lc-rs"] }
data-encoding = "2.11.0"
uuid = { version = "1.25.0", features = ["v4", "fast-rng", "serde"] }
rustls = { version = "0.23.43", features = ["aws-lc-rs"] }
data-encoding = "2.11.1"
urlencoding = "2.1.3"
bcrypt = "0.19.2"
bcrypt = "0.19.3"
hmac = "0.13.0"
sha1 = "0.11.0"
sha2 = "0.11.0"
@@ -112,26 +112,26 @@ hickory-resolver = "0.26.1"
portable-pty = "0.9.0"
shell-escape = "0.1.5"
crossterm = "0.29.0"
bollard = "0.21.0"
bollard = "0.21.1"
sysinfo = "0.39.6"
shlex = "2.0.1"
# CLOUD
aws-config = "1.10.0"
aws-sdk-ec2 = "1.241.0"
aws-config = "1.11.0"
aws-sdk-ec2 = "1.252.0"
aws-credential-types = "1.3.0"
## CRON
english-to-cron = "0.1.7"
chrono-tz = "0.10.4"
chrono = "0.4.45"
croner = "3.0.1"
croner = "4.0.0"
# MISC
async-compression = { version = "0.4.42", features = ["tokio", "gzip"] }
schemars = { version = "1.2.1", features = ["indexmap2"] }
async-compression = { version = "0.4.43", features = ["tokio", "gzip"] }
schemars = { version = "1.2.2", features = ["indexmap2"] }
derive_builder = "0.20.2"
comfy-table = "7.2.2"
comfy-table = "8.0.0"
typeshare = "1.0.5"
wildcard = "0.3.0"
colored = "3.1.1"
+1 -5
View File
@@ -1,5 +1 @@
{
"imports": {
"@std/toml": "jsr:@std/toml"
}
}
{}
+1 -42
View File
@@ -1,5 +1,3 @@
import * as TOML from "@std/toml";
export const run = async (action: string) => {
const branch = await new Deno.Command("bash", {
args: ["-c", "git rev-parse --abbrev-ref HEAD"],
@@ -7,46 +5,7 @@ export const run = async (action: string) => {
.output()
.then((r) => new TextDecoder("utf-8").decode(r.stdout).trim());
const cargo_toml_str = await Deno.readTextFile("Cargo.toml");
const prev_version = (
TOML.parse(cargo_toml_str) as {
workspace: { package: { version: string } };
}
).workspace.package.version;
const [version, tag, count] = prev_version.split("-");
const next_count = Number(count) + 1;
const next_version = `${version}-${tag}-${next_count}`;
await Deno.writeTextFile(
"Cargo.toml",
cargo_toml_str.replace(
`version = "${prev_version}"`,
`version = "${next_version}"`
)
);
// Cargo check first here to make sure lock file is updated before commit.
const cmd = `
cargo check
echo ""
git add --all
git commit --all --message "deploy ${version}-${tag}-${next_count}"
echo ""
git push
echo ""
km run -y action ${action} "KOMODO_BRANCH=${branch}&KOMODO_VERSION=${version}&KOMODO_TAG=${tag}-${next_count}"
`
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("//"))
.join(" && ");
new Deno.Command("bash", {
args: ["-c", cmd],
args: ["-c", `km run -y action ${action} "BRANCH=${branch}"`],
}).spawn();
};
+7 -2
View File
@@ -1,8 +1,8 @@
## Builds the Komodo Core, Periphery, and Util binaries
## for a specific architecture. Requires OpenSSL 3 or later.
FROM rust:1.97.1-bookworm AS builder
RUN cargo install cargo-strip
FROM rust:1.98.0-bookworm AS builder
RUN cargo install cargo-strip cargo-edit
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -14,6 +14,11 @@ COPY ./bin/periphery ./bin/periphery
COPY ./bin/cli ./bin/cli
COPY ./xtask ./xtask
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_core ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
# Compile bin
RUN \
cargo build -p komodo_core --release && \
+12 -4
View File
@@ -3,7 +3,7 @@
## Uses chef for dependency caching to help speed up back-to-back builds.
FROM lukemathwalker/cargo-chef:latest-rust-1.97.1-bookworm AS chef
FROM lukemathwalker/cargo-chef:latest-rust-1.98.0-bookworm AS chef
WORKDIR /builder
# Plan just the RECIPE to see if things have changed
@@ -12,12 +12,20 @@ COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
RUN cargo install cargo-strip
COPY --from=planner /builder/recipe.json recipe.json
RUN cargo install cargo-strip cargo-edit
# Build JUST dependencies - cached layer
COPY --from=planner /builder/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
# NOW copy again (this time into builder) and build app
# NOW copy again (this time into builder), set version, and build app
COPY . .
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_core ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
RUN \
cargo build --release --bin core && \
cargo build --release --bin periphery && \
+7 -2
View File
@@ -1,5 +1,5 @@
FROM rust:1.97.1-trixie AS builder
RUN cargo install cargo-strip
FROM rust:1.98.0-trixie AS builder
RUN cargo install cargo-strip cargo-edit
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -8,6 +8,11 @@ COPY ./client/core/rs ./client/core/rs
COPY ./client/periphery ./client/periphery
COPY ./bin/cli ./bin/cli
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_cli ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
# Compile bin
RUN cargo build -p komodo_cli --release && cargo strip
+2 -2
View File
@@ -100,7 +100,7 @@ fn print_items<T: PrintTable + Serialize>(
match format {
CliFormat::Table => {
let mut table = Table::new();
let preset = {
let style = {
use comfy_table::presets::*;
match cli_config().table_borders {
None | Some(CliTableBorders::Horizontal) => {
@@ -112,7 +112,7 @@ fn print_items<T: PrintTable + Serialize>(
Some(CliTableBorders::All) => UTF8_FULL,
}
};
table.load_preset(preset).set_header(
table.load_style(style).set_header(
T::header(links)
.iter()
.map(|h| Cell::new(h).add_attribute(Attribute::Bold)),
+7 -2
View File
@@ -1,8 +1,8 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
# Build Core
FROM rust:1.97.1-trixie AS core-builder
RUN cargo install cargo-strip
FROM rust:1.98.0-trixie AS core-builder
RUN cargo install cargo-strip cargo-edit
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -13,6 +13,11 @@ COPY ./bin/core ./bin/core
COPY ./bin/cli ./bin/cli
COPY ./xtask ./xtask
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_core ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
# Compile app
RUN cargo build -p komodo_core --release && \
cargo build -p komodo_cli --release && \
+50 -91
View File
@@ -13,6 +13,7 @@ use komodo_client::{
permission::PermissionLevel,
procedure::{Procedure, ProcedureStage},
repo::Repo,
resource::ResourceQuery,
stack::Stack,
update::Update,
user::procedure_user,
@@ -33,7 +34,10 @@ use crate::{
state::{all_resources_cache, db_client},
};
use super::update::{init_execution_update, update_update};
use super::{
query::get_all_tags,
update::{init_execution_update, update_update},
};
pub async fn execute_procedure(
procedure: &Procedure,
@@ -109,95 +113,41 @@ async fn execute_procedure_stage(
update: &Mutex<Update>,
) -> anyhow::Result<()> {
let mut executions = Vec::with_capacity(_executions.capacity());
for execution in _executions {
match execution {
Execution::BatchRunAction(exec) => {
extend_batch_exection::<BatchRunAction>(
&exec.pattern,
&mut executions,
)
.await?;
// Expands the batch executions into their matching
// single executions, and passes through the rest.
macro_rules! expand_batch_executions {
($($Variant:ident),* $(,)?) => {
for execution in _executions {
match execution {
$(
Execution::$Variant(exec) => {
extend_batch_execution::<$Variant>(
&exec.pattern,
&exec.tags,
&mut executions,
)
.await?;
}
)*
execution => executions.push(execution),
}
}
Execution::BatchRunProcedure(exec) => {
extend_batch_exection::<BatchRunProcedure>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchRunBuild(exec) => {
extend_batch_exection::<BatchRunBuild>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchCloneRepo(exec) => {
extend_batch_exection::<BatchCloneRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchPullRepo(exec) => {
extend_batch_exection::<BatchPullRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchBuildRepo(exec) => {
extend_batch_exection::<BatchBuildRepo>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeploy(exec) => {
extend_batch_exection::<BatchDeploy>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyDeployment(exec) => {
extend_batch_exection::<BatchDestroyDeployment>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStack(exec) => {
extend_batch_exection::<BatchDeployStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDeployStackIfChanged(exec) => {
extend_batch_exection::<BatchDeployStackIfChanged>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchPullStack(exec) => {
extend_batch_exection::<BatchPullStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
Execution::BatchDestroyStack(exec) => {
extend_batch_exection::<BatchDestroyStack>(
&exec.pattern,
&mut executions,
)
.await?;
}
execution => executions.push(execution),
}
};
}
expand_batch_executions!(
BatchRunAction,
BatchRunProcedure,
BatchRunBuild,
BatchCloneRepo,
BatchPullRepo,
BatchBuildRepo,
BatchDeploy,
BatchDestroyDeployment,
BatchDeployStack,
BatchDeployStackIfChanged,
BatchPullStack,
BatchDestroyStack,
);
let futures = executions.into_iter().map(|execution| async move {
let now = Instant::now();
add_line_to_update(
@@ -550,18 +500,27 @@ async fn add_line_to_update(update: &Mutex<Update>, line: &str) {
};
}
async fn extend_batch_exection<E: ExtendBatch>(
async fn extend_batch_execution<E: ExtendBatch>(
pattern: &str,
tags: &[String],
executions: &mut Vec<Execution>,
) -> anyhow::Result<()> {
let all_tags = if tags.is_empty() {
Vec::new()
} else {
get_all_tags(None).await?
};
let more = list_full_for_user_using_pattern::<E::Resource>(
pattern,
Default::default(),
ResourceQuery {
tags: tags.to_vec(),
..Default::default()
},
None,
None,
procedure_user(),
PermissionLevel::Read.into(),
&[],
&all_tags,
)
.await?
.into_iter()
+116 -119
View File
@@ -9,24 +9,26 @@
//! - Automatic container environment detection
//! - Interface validation (existence and UP state)
//! - Gateway discovery from routing tables or network configuration
//! - Safe default route modification with privilege checking
//! - Atomic default route replacement (never leaves the container without a default route)
//! - Comprehensive error handling and logging
use std::net::Ipv4Addr;
use anyhow::{Context, anyhow};
use tokio::process::Command;
use tracing::{debug, info, trace, warn};
/// Standard gateway addresses to test for Docker networks
const DOCKER_GATEWAY_CANDIDATES: &[&str] = &[".1", ".254"];
/// Container environment detection files
const DOCKERENV_FILE: &str = "/.dockerenv";
const CONTAINERENV_FILE: &str = "/run/.containerenv";
const CGROUP_FILE: &str = "/proc/1/cgroup";
/// Check if running in container environment
fn is_container_environment() -> bool {
// Check for Docker-specific indicators
if std::path::Path::new(DOCKERENV_FILE).exists() {
// Check for Docker / Podman specific indicators
if std::path::Path::new(DOCKERENV_FILE).exists()
|| std::path::Path::new(CONTAINERENV_FILE).exists()
{
return true;
}
@@ -35,7 +37,9 @@ fn is_container_environment() -> bool {
return true;
}
// Check cgroup for container runtime indicators
// Check cgroup for container runtime indicators.
// Only reliable on cgroup v1 - on cgroup v2 the file
// is usually just `0::/`, so the checks above carry detection.
if let Ok(content) = std::fs::read_to_string(CGROUP_FILE)
&& (content.contains("docker") || content.contains("containerd"))
{
@@ -51,23 +55,24 @@ pub async fn configure_internet_gateway() {
let config = core_config();
if config.internet_interface.is_empty() {
debug!("No interface specified, using default routing");
return;
}
if !is_container_environment() {
debug!("Not in container, skipping network configuration");
return;
}
if !config.internet_interface.is_empty() {
debug!(
"Configuring internet interface: {}",
config.internet_interface
);
if let Err(e) =
configure_manual_interface(&config.internet_interface).await
{
warn!("Failed to configure internet gateway: {e:#}");
}
} else {
debug!("No interface specified, using default routing");
debug!(
"Configuring internet interface: {}",
config.internet_interface
);
if let Err(e) =
configure_manual_interface(&config.internet_interface).await
{
warn!("Failed to configure internet gateway: {e:#}");
}
}
@@ -91,7 +96,10 @@ async fn configure_manual_interface(
let interface_info =
String::from_utf8_lossy(&interface_check.stdout);
if !interface_info.contains("state UP") {
// tun / wireguard style interfaces report `state UNKNOWN` while operational
if !interface_info.contains("state UP")
&& !interface_info.contains("state UNKNOWN")
{
return Err(anyhow!(
"Interface '{}' is not UP. Please ensure the interface is enabled and connected",
interface_name
@@ -159,7 +167,7 @@ async fn find_gateway(
.context("Failed to get routes for interface")?;
if route_output.status.success() {
let routes = String::from_utf8(route_output.stdout)?;
let routes = String::from_utf8_lossy(&route_output.stdout);
trace!("Routes for {}: {}", interface_name, routes.trim());
// Look for routes with gateway
@@ -179,60 +187,75 @@ async fn find_gateway(
}
}
// Derive gateway from network configuration (Docker standard: .1)
if let Some(network_base) = ip_cidr.split('/').next() {
let ip_parts: Vec<&str> = network_base.split('.').collect();
if ip_parts.len() == 4 {
let potential_gateways: Vec<String> = DOCKER_GATEWAY_CANDIDATES
.iter()
.map(|suffix| {
format!(
"{}.{}.{}{}",
ip_parts[0], ip_parts[1], ip_parts[2], suffix
)
})
.collect();
// Derive candidates from the interface subnet
let candidates = derive_gateway_candidates(ip_cidr)
.with_context(|| {
format!(
"Could not determine gateway for interface '{interface_name}' in network '{ip_cidr}'"
)
})?;
for gateway in potential_gateways {
trace!(
"Testing potential gateway {} for {}",
gateway, interface_name
);
for gateway in &candidates {
trace!(
"Testing potential gateway {} for {}",
gateway, interface_name
);
// Check if gateway is reachable
let route_test = Command::new("ip")
.args(["route", "get", &gateway, "dev", interface_name])
.output()
.await;
// Note: `ip route get` only confirms the address is on-link
// for this interface, not that a router actually answers there.
let route_test = Command::new("ip")
.args(["route", "get", gateway, "dev", interface_name])
.output()
.await;
if let Ok(output) = route_test
&& output.status.success()
{
trace!(
"Gateway {} is reachable via {}",
gateway, interface_name
);
return Ok(gateway.to_string());
}
// Fallback: assume .1 is gateway (Docker standard)
if gateway.ends_with(".1") {
trace!(
"Assuming Docker gateway {} for {}",
gateway, interface_name
);
return Ok(gateway.to_string());
}
}
if let Ok(output) = route_test
&& output.status.success()
{
trace!("Gateway {} is on-link via {}", gateway, interface_name);
return Ok(gateway.clone());
}
}
Err(anyhow!(
"Could not determine gateway for interface '{}' in network '{}'. \
Ensure the interface is properly configured with a valid gateway",
interface_name,
ip_cidr
))
// Fall back to the first host address (Docker standard)
let gateway = candidates
.into_iter()
.next()
.context("No gateway candidates derived")?;
trace!(
"Assuming Docker gateway {} for {}",
gateway, interface_name
);
Ok(gateway)
}
/// Derive candidate gateway addresses for the interface subnet:
/// the first and last usable host addresses. Docker assigns the
/// gateway the first host address of the subnet regardless of prefix length.
fn derive_gateway_candidates(
ip_cidr: &str,
) -> anyhow::Result<Vec<String>> {
let (ip, prefix) = ip_cidr
.split_once('/')
.context("Address is not in CIDR (ip/prefix) format")?;
let ip: Ipv4Addr =
ip.parse().context("Failed to parse IPv4 address")?;
let prefix: u32 =
prefix.parse().context("Failed to parse CIDR prefix")?;
if !(1..=30).contains(&prefix) {
return Err(anyhow!(
"Subnet /{prefix} cannot contain a distinct gateway address"
));
}
let mask = u32::MAX << (32 - prefix);
let network = u32::from(ip) & mask;
let first_host = network + 1;
let last_host = (network | !mask) - 1;
Ok(vec![
Ipv4Addr::from(first_host).to_string(),
Ipv4Addr::from(last_host).to_string(),
])
}
/// Set default gateway to use specified interface
@@ -245,48 +268,33 @@ async fn set_default_gateway(
gateway, interface_name
);
// Check if we have network privileges
if !check_network_privileges().await {
warn!(
"⚠️ Container lacks network privileges (NET_ADMIN capability required)"
);
warn!(
"Add 'cap_add: [\"NET_ADMIN\"]' to your docker-compose.yaml"
);
return Err(anyhow!(
"Insufficient network privileges to modify routing table. \
Container needs NET_ADMIN capability to configure network interfaces"
));
}
// Remove existing default routes
let remove_default = Command::new("sh")
.args(["-c", "ip route del default 2>/dev/null || true"])
.output()
.await;
if let Ok(output) = remove_default
&& output.status.success()
{
trace!("Removed existing default routes");
}
// Add new default route
let add_default_cmd = format!(
"ip route add default via {gateway} dev {interface_name}"
);
trace!("Adding default route: {}", add_default_cmd);
let add_default = Command::new("sh")
.args(["-c", &add_default_cmd])
// `replace` swaps the default route atomically, so a failure
// never leaves the container without any default route.
let replace = Command::new("ip")
.args([
"route",
"replace",
"default",
"via",
gateway,
"dev",
interface_name,
])
.output()
.await
.context("Failed to add default route")?;
.context("Failed to run ip route replace")?;
if !add_default.status.success() {
let error = String::from_utf8_lossy(&add_default.stderr)
.trim()
.to_string();
if !replace.status.success() {
let error =
String::from_utf8_lossy(&replace.stderr).trim().to_string();
if error.contains("Operation not permitted") {
warn!(
"⚠️ Container lacks network privileges (NET_ADMIN capability required)"
);
warn!(
"Add 'cap_add: [\"NET_ADMIN\"]' to your docker-compose.yaml"
);
}
return Err(anyhow!(
"❌ Failed to set default gateway via '{}': {}. \
Verify interface configuration and network permissions",
@@ -298,14 +306,3 @@ async fn set_default_gateway(
trace!("Default gateway set to {} via {}", gateway, interface_name);
Ok(())
}
/// Check if we have sufficient network privileges
async fn check_network_privileges() -> bool {
// Try to test NET_ADMIN capability with a harmless route operation
let capability_test = Command::new("sh")
.args(["-c", "ip route add 198.51.100.1/32 dev lo 2>/dev/null && ip route del 198.51.100.1/32 dev lo 2>/dev/null"])
.output()
.await;
matches!(capability_test, Ok(output) if output.status.success())
}
+18 -9
View File
@@ -1,6 +1,6 @@
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::Context;
use anyhow::{Context, anyhow};
use async_timing_util::{Timelength, wait_until_timelength};
use komodo_client::entities::{
ResourceTargetVariant, report::KomodoReport,
@@ -60,12 +60,12 @@ async fn report(
.get("https://mogh.tech/report/public_key")
.send()
.await
.context("Failed to query for reporting endpoint public key for signature.")?
.context("Failed to query for reporting endpoint public key for signature")?
.error_for_status()
.context("Failed response for reporting endpoint public key for signature.")?
.context("Failed response for reporting endpoint public key for signature")?
.text()
.await
.context("Failed to get reporting endpoint public key for signature.")?;
.context("Failed to get reporting endpoint public key for signature")?;
let endpoint_public_key =
SpkiPublicKey::maybe_pem_to_raw_bytes(&endpoint_public_key)
@@ -138,7 +138,7 @@ async fn report(
.generate_signature()
.context("Failed to generate report signature")?;
client
let res = client
.post("https://mogh.tech/report/komodo")
.header("x-api-signature", signature)
.header("x-api-timestamp", timestamp)
@@ -146,9 +146,18 @@ async fn report(
.body(serialized)
.send()
.await
.context("Failed to post report.")?
.error_for_status()
.context("Failed response for report post.")?;
.context("Failed to post report")?;
Ok(())
let status = res.status();
if status.is_success() {
return Ok(());
}
let text = res
.text()
.await
.context("Failed report, and failed to get response body")?;
Err(anyhow!("{status}: {text}"))
}
+11 -9
View File
@@ -145,16 +145,18 @@ impl super::KomodoResource for Stack {
})
.unwrap_or_default();
let default_git = (
stack.config.git_provider,
stack.config.repo,
stack.config.branch,
stack.config.git_https,
String::new(),
);
let default_git = || {
(
stack.config.git_provider,
stack.config.repo,
stack.config.branch,
stack.config.git_https,
String::new(),
)
};
let (git_provider, repo, branch, git_https, linked_repo_name) =
if stack.config.linked_repo.is_empty() {
default_git
default_git()
} else {
all_resources_cache()
.load()
@@ -169,7 +171,7 @@ impl super::KomodoResource for Stack {
r.name.clone(),
)
})
.unwrap_or(default_git)
.unwrap_or_else(default_git)
};
// This is only true if it is KNOWN to be true. so other cases are false.
+7 -2
View File
@@ -109,14 +109,19 @@ pub async fn on_startup() {
// Configure manual network interface if specified
network::configure_internet_gateway().await;
// Run legacy database fixes
tokio::join!(
clean_up_server_templates(),
v2_init_missing_resource_info(),
);
// Standard startup tasks
tokio::join!(
in_progress_update_cleanup(),
open_alert_cleanup(),
action_api_key_cleanup(),
ensure_first_server_and_builder(),
ensure_init_user_and_resources(),
clean_up_server_templates(),
v2_init_missing_resource_info(),
);
}
+7 -2
View File
@@ -1,7 +1,7 @@
## All in one, multi stage compile + runtime Docker build for your architecture.
FROM rust:1.97.1-trixie AS builder
RUN cargo install cargo-strip
FROM rust:1.98.0-trixie AS builder
RUN cargo install cargo-strip cargo-edit
WORKDIR /builder
COPY Cargo.toml Cargo.lock ./
@@ -11,6 +11,11 @@ COPY ./client/periphery ./client/periphery
COPY ./bin/periphery ./bin/periphery
COPY ./xtask ./xtask
# Set Version
ARG VERSION="0.0.0"
ARG IMAGE_TAG=""
RUN cargo set-version -p komodo_periphery ${VERSION}${IMAGE_TAG:+-${IMAGE_TAG}}
# Compile app
RUN cargo build -p komodo_periphery --release && cargo strip
+23 -2
View File
@@ -304,15 +304,36 @@ impl Resolve<crate::api::Args> for build::Build {
// Do this in fallible block to ensure
// cancel token removed from cache on failure.
let command = async {
// Add VERSION to build args (if not already there)
let mut build_args = environment_vars_from_str(build_args)
.context("Invalid build_args")?;
.context("Invalid build_args")?;
// Add IMAGE_NAME to build args
if !build_args.iter().any(|a| a.variable == "IMAGE_NAME") {
build_args.push(EnvironmentVar {
variable: String::from("IMAGE_NAME"),
value: if build.config.image_name.is_empty() {
build.name.clone()
} else {
build.config.image_name.clone()
},
});
}
// Maybe Add IMAGE_TAG to build args
if !build.config.image_tag.is_empty()
&& !build_args.iter().any(|a| a.variable == "IMAGE_TAG") {
build_args.push(EnvironmentVar {
variable: String::from("IMAGE_TAG"),
value: build.config.image_tag.clone(),
});
}
// Add VERSION to build args
if !build_args.iter().any(|a| a.variable == "VERSION") {
build_args.push(EnvironmentVar {
variable: String::from("VERSION"),
value: build.config.version.to_string(),
});
}
let build_args = parse_build_args(&build_args);
let secret_args = environment_vars_from_str(secret_args)
+2 -2
View File
@@ -61,7 +61,7 @@ impl Resolve<crate::api::Args> for GetComposeLog {
run_komodo_standard_command(
"Get Stack Log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await,
)
@@ -101,7 +101,7 @@ impl Resolve<crate::api::Args> for GetComposeLogSearch {
run_komodo_shell_command(
"Search Stack Log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await,
)
+2 -2
View File
@@ -69,7 +69,7 @@ impl Resolve<crate::api::Args> for GetContainerLog {
run_komodo_standard_command(
"Get container log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await,
)
@@ -104,7 +104,7 @@ impl Resolve<crate::api::Args> for GetContainerLogSearch {
run_komodo_shell_command(
"Get container log grep",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await,
)
+2 -2
View File
@@ -90,7 +90,7 @@ impl Resolve<crate::api::Args> for GetSwarmServiceLog {
run_komodo_standard_command(
"Get Swarm Service Log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await,
)
@@ -141,7 +141,7 @@ impl Resolve<crate::api::Args> for GetSwarmServiceLogSearch {
run_komodo_shell_command(
"Search Swarm Service Log",
command,
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await,
)
+1 -1
View File
@@ -21,7 +21,7 @@ pub async fn list_compose_projects()
let res = run_komodo_standard_command(
"List Projects",
format!("{docker_compose} ls --all --format json"),
CommandOptions::default().timeout(Duration::from_secs(1)),
CommandOptions::default().timeout(Duration::from_secs(5)),
)
.await;
+2 -2
View File
@@ -26,7 +26,7 @@ pub async fn list_swarm_configs(
let res = run_komodo_standard_command(
"List Swarm Configs",
"docker config ls --format json",
CommandOptions::default().timeout(Duration::from_secs(1)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await;
@@ -73,7 +73,7 @@ pub async fn inspect_swarm_config(
// `--` so a name beginning with `-` is not parsed as a flag.
// The quotes alone give no protection, as they are stripped when lexed.
format!(r#"docker config inspect -- "{config}""#),
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await;
+3 -3
View File
@@ -74,7 +74,7 @@ pub async fn list_swarm_stacks(
let res = run_komodo_standard_command(
"List Swarm Stacks",
"docker stack ls --format json",
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await;
@@ -133,7 +133,7 @@ pub async fn list_swarm_stack_service_ids(
let res = run_komodo_standard_command(
"List Swarm Stack Services",
format!("docker stack services --format json -- {stack}"),
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await;
@@ -163,7 +163,7 @@ pub async fn list_swarm_stack_tasks(
let res = run_komodo_standard_command(
"List Swarm Stack Tasks",
format!("docker stack ps --format json --no-trunc -- {stack}"),
CommandOptions::default().timeout(Duration::from_secs(3)),
CommandOptions::default().timeout(Duration::from_secs(10)),
)
.await;
-1
View File
@@ -1,6 +1,5 @@
use anyhow::Context;
use bollard::query_parameters::ListTasksOptions;
use komodo_client::entities::docker::task::*;
use super::*;
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "komodo_client"
description = "Client for the Komodo build and deployment system"
version.workspace = true
version = "2.3.3"
edition.workspace = true
license.workspace = true
homepage.workspace = true
+8 -6
View File
@@ -256,14 +256,14 @@ impl MaybeNone for BuilderConfigDiff {
impl From<PartialBuilderConfig> for BuilderConfig {
fn from(value: PartialBuilderConfig) -> BuilderConfig {
match value {
PartialBuilderConfig::Url(server) => {
BuilderConfig::Url(server.into())
PartialBuilderConfig::Url(config) => {
BuilderConfig::Url(config.into())
}
PartialBuilderConfig::Server(server) => {
BuilderConfig::Server(server.into())
PartialBuilderConfig::Server(config) => {
BuilderConfig::Server(config.into())
}
PartialBuilderConfig::Aws(builder) => {
BuilderConfig::Aws(builder.into())
PartialBuilderConfig::Aws(config) => {
BuilderConfig::Aws(config.into())
}
}
}
@@ -469,11 +469,13 @@ pub struct ServerBuilderConfig {
deserialize_with = "string_list_deserializer"
)]
#[partial_attr(serde(
default,
alias = "server_id",
alias = "server",
alias = "servers",
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
#[cfg_attr(
feature = "schemars",
partial_attr(schemars(rename = "servers"))
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "2.3.1",
"version": "2.3.3",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
@@ -13,10 +13,10 @@
"build": "tsc"
},
"dependencies": {
"mogh_auth_client": "^1.7.0"
"mogh_auth_client": "^1.7.1"
},
"devDependencies": {
"typescript": "^6.0.2"
"typescript": "^7.0.2"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+129 -8
View File
@@ -2,19 +2,140 @@
# yarn lockfile v1
"@typescript/typescript-aix-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz#cdc7ce81d60f1e09034960ddfb1fb880d7a776b6"
integrity sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==
"@typescript/typescript-darwin-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz#a55fdfcfa58df58d27db2237cde6a5c1e35a7235"
integrity sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==
"@typescript/typescript-darwin-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz#38d1c9172800a91d707bec64d2a370a016634db4"
integrity sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==
"@typescript/typescript-freebsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz#f1ff8810030b35d2b5be0db6a2dc650460ea94fa"
integrity sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==
"@typescript/typescript-freebsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz#3d86b03f353c5b1ba95162eb6ce35533bfc294bd"
integrity sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==
"@typescript/typescript-linux-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz#d9334d96d6dac6ff85da9c865588948de939e91f"
integrity sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==
"@typescript/typescript-linux-arm@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz#ad94b41e1aee2a4dcc6a298c7b67c43345fde32e"
integrity sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==
"@typescript/typescript-linux-loong64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz#2965aee4fc873360139d893daafe6397a29138ad"
integrity sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==
"@typescript/typescript-linux-mips64el@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz#1a887a311bed3a833f80bfd4a9ed37c271936cf0"
integrity sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==
"@typescript/typescript-linux-ppc64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz#8b63c9b2f445b393eb4e43ec21da225dade3577d"
integrity sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==
"@typescript/typescript-linux-riscv64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz#b6e8a35c289b3ea97a92a41d461aaeed0d3b36e1"
integrity sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==
"@typescript/typescript-linux-s390x@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz#2ef96693be4861f6d17965427e5b009cbbed1a3e"
integrity sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==
"@typescript/typescript-linux-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz#73269cb0baba50aea0ca060445a6b88e583f1ce2"
integrity sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==
"@typescript/typescript-netbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz#3a3649f97fafa210b4e6e3798c15e06605c8a901"
integrity sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==
"@typescript/typescript-netbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz#47ec59491a40c470d2807dc4d2b825528fd979ab"
integrity sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==
"@typescript/typescript-openbsd-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz#796be8da0bd989d8a3fb96f2801e38a8365b4baf"
integrity sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==
"@typescript/typescript-openbsd-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz#d37fe2a729eb942c076c454ee7f1815faf7d560f"
integrity sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==
"@typescript/typescript-sunos-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz#aba8d3464c3565a7044789baba96916bd4ab2c88"
integrity sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==
"@typescript/typescript-win32-arm64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz#b9de50a17196383f62620b5f9d0a2f34ad3b60d7"
integrity sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==
"@typescript/typescript-win32-x64@7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz#cf3b7b0d6ce5635daca4c8e01c189cdcde47ec3c"
integrity sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==
jwt-decode@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
mogh_auth_client@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.7.0.tgz#a016c8dafd0f73633098d41aa95889c444e481d9"
integrity sha512-v4TJVNIiFYBLd70yGw/eS37E15he4I5w3poj5X7W75D+m6ry9gbhtlePp5elRbqjDZ/WXkOZ/XYCHqGYb/PQ2A==
mogh_auth_client@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/mogh_auth_client/-/mogh_auth_client-1.7.1.tgz#98469ee4cff4c47c991235acd0b96c1b27c5a1e4"
integrity sha512-IWT7ET5QmqNGqrGgNy4oyF4LMb1TeLXQhDw6AXzd5S/WfWC9JJx7xjnIdJFqXpl3zoZ+va773RrPB5ufGPkVVQ==
dependencies:
jwt-decode "^4.0.0"
typescript@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.2.tgz#0b1bfb15f68c64b97032f3d78abbf98bdbba501f"
integrity sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==
typescript@^7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-7.0.2.tgz#9ec773d7954a8c182c17cc5bbd575aa28bc51582"
integrity sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==
optionalDependencies:
"@typescript/typescript-aix-ppc64" "7.0.2"
"@typescript/typescript-darwin-arm64" "7.0.2"
"@typescript/typescript-darwin-x64" "7.0.2"
"@typescript/typescript-freebsd-arm64" "7.0.2"
"@typescript/typescript-freebsd-x64" "7.0.2"
"@typescript/typescript-linux-arm" "7.0.2"
"@typescript/typescript-linux-arm64" "7.0.2"
"@typescript/typescript-linux-loong64" "7.0.2"
"@typescript/typescript-linux-mips64el" "7.0.2"
"@typescript/typescript-linux-ppc64" "7.0.2"
"@typescript/typescript-linux-riscv64" "7.0.2"
"@typescript/typescript-linux-s390x" "7.0.2"
"@typescript/typescript-linux-x64" "7.0.2"
"@typescript/typescript-netbsd-arm64" "7.0.2"
"@typescript/typescript-netbsd-x64" "7.0.2"
"@typescript/typescript-openbsd-arm64" "7.0.2"
"@typescript/typescript-openbsd-x64" "7.0.2"
"@typescript/typescript-sunos-x64" "7.0.2"
"@typescript/typescript-win32-arm64" "7.0.2"
"@typescript/typescript-win32-x64" "7.0.2"
-19
View File
@@ -498,25 +498,6 @@ webhook_secret = "a_random_webhook_secret"
## Default: empty (none)
webhook_base_url = ""
## Configure Github webhook app. Enables webhook management apis.
## <INSERT LINK TO GUIDE>
## Env: KOMODO_GITHUB_WEBHOOK_APP_APP_ID or KOMODO_GITHUB_WEBHOOK_APP_APP_ID_FILE
# github_webhook_app.app_id = 1234455 # Find on the app page.
## Env:
## - KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_IDS or KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_IDS_FILE
## - KOMODO_GITHUB_WEBHOOK_APP_INSTALLATIONS_NAMESPACES
# github_webhook_app.installations = [
# ## Find the id after installing the app to user / organization. "namespace" is the username / organization name.
# { id = 1234, namespace = "mbecker20" }
# ]
## The path to Github webhook app private key. <INSERT LINK TO GUIDE>
## This is defaulted to `/github/private-key.pem`, and doesn't need to be changed if running core in Docker.
## Just mount the private key pem file on the host to `/github/private-key.pem` in the container.
## Eg. `/your/path/to/key.pem : /github/private-key.pem`
## Env: KOMODO_GITHUB_WEBHOOK_APP_PK_PATH
# github_webhook_app.pk_path = "/path/to/pk.pem"
###########
# LOGGING #
###########
+14 -6
View File
@@ -1,5 +1,8 @@
FROM node:22.12-alpine AS builder
ARG DOCSITE_URL
ARG DOCSITE_BASE_URL=/
WORKDIR /builder
COPY package.json yarn.lock ./
@@ -13,19 +16,24 @@ COPY ./docusaurus.config.ts ./docusaurus.config.ts
COPY ./sidebars.ts ./sidebars.ts
COPY ./tsconfig.json ./tsconfig.json
RUN yarn build
RUN DOCSITE_URL=${DOCSITE_URL} DOCSITE_BASE_URL=${DOCSITE_BASE_URL} yarn build
FROM caddy:latest
# Copy into caddy image /srv
COPY --from=builder /builder/build /srv
ARG DOCSITE_BASE_URL=/
# Add a basic caddyfile serving http
RUN <<EOF cat > /etc/caddy/Caddyfile
# Copy into caddy image /srv, nested under the base url path
COPY --from=builder /builder/build /srv${DOCSITE_BASE_URL}
# Add a basic caddyfile serving http.
# BASE is the base url without its trailing slash (empty when base url is "/"),
# used to redirect e.g. /komodo -> /komodo/ so relative urls resolve correctly.
RUN BASE="${DOCSITE_BASE_URL%/}" && cat > /etc/caddy/Caddyfile <<EOF
:80 {
root * /srv
${BASE:+redir $BASE $BASE/ 308}
file_server
try_files {path} /index.html
try_files {path} {path}.html ${DOCSITE_BASE_URL}index.html
}
EOF
+2 -3
View File
@@ -11,11 +11,10 @@ const config: Config = {
favicon: "img/favicon.ico",
// Set the production url of your site here
url: "https://komo.do",
url: process.env.DOCSITE_URL || "https://komo.do",
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
// baseUrl: "/komodo/",
baseUrl: "/",
baseUrl: process.env.DOCSITE_BASE_URL || "/",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.97.1-trixie as builder
FROM rust:1.98.0-trixie as builder
WORKDIR /builder
COPY . .
+1 -1
View File
@@ -1,4 +1,4 @@
FROM rust:1.97.1-trixie as builder
FROM rust:1.98.0-trixie as builder
WORKDIR /builder
COPY . .
+1 -1
View File
@@ -7,7 +7,7 @@ use command::{CommandOptions, run_standard_command};
pub async fn check_installed() -> anyhow::Result<()> {
if run_standard_command(
"which git",
CommandOptions::default().timeout(Duration::from_secs(1)),
CommandOptions::default().timeout(Duration::from_secs(2)),
)
.await
.success()
+3 -3
View File
@@ -31,7 +31,7 @@ pub async fn get_commit_hash_info(
"git rev-parse --short HEAD",
CommandOptions::default()
.path(repo_dir)
.timeout(Duration::from_secs(1)),
.timeout(Duration::from_secs(2)),
)
.await;
let hash = if hash.status.success() {
@@ -46,7 +46,7 @@ pub async fn get_commit_hash_info(
"git log -1 --pretty=%B",
CommandOptions::default()
.path(repo_dir)
.timeout(Duration::from_secs(1)),
.timeout(Duration::from_secs(2)),
)
.await;
let message = if message.status.success() {
@@ -93,7 +93,7 @@ pub async fn get_remote_url(path: &Path) -> anyhow::Result<String> {
"git remote show origin",
CommandOptions::default()
.path(path)
.timeout(Duration::from_secs(1)),
.timeout(Duration::from_secs(2)),
)
.await;
if output.success() {
+25 -25
View File
@@ -10,48 +10,48 @@
"build-client": "cd ../client/core/ts && yarn && yarn build && yarn link"
},
"dependencies": {
"@mantine/core": "^9.4.1",
"@mantine/form": "^9.4.1",
"@mantine/hooks": "^9.4.1",
"@mantine/notifications": "^9.4.1",
"@mantine/spotlight": "^9.4.1",
"@mantine/core": "^9.5.2",
"@mantine/form": "^9.5.2",
"@mantine/hooks": "^9.5.2",
"@mantine/notifications": "^9.5.2",
"@mantine/spotlight": "^9.5.2",
"@monaco-editor/react": "^4.7.0",
"@tanstack/react-query": "^5.101.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-query": "^5.102.4",
"@tanstack/react-table": "^9.1.2",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"ansi-to-html": "^0.7.2",
"jotai": "^2.20.1",
"jotai-family": "^1.0.2",
"jotai": "^2.20.3",
"jotai-family": "^1.1.0",
"jotai-location": "^0.6.2",
"lucide-react": "^1.22.0",
"mogh_auth_client": "^1.7.0",
"mogh_ui": "^0.7.6",
"lucide-react": "^1.34.0",
"mogh_auth_client": "^1.7.1",
"mogh_ui": "^1.2.7",
"monaco-editor": "^0.56.0",
"monaco-yaml": "^5.5.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-minimal-pie-chart": "^9.1.2",
"react-router-dom": "^7.18.1",
"react-router-dom": "^7.18.2",
"react-xtermjs": "^1.0.12",
"recharts": "^3.9.1",
"sanitize-html": "^2.17.5",
"shell-quote": "^1.9.0"
"recharts": "^3.10.1",
"sanitize-html": "^2.17.7",
"shell-quote": "^1.10.0"
},
"devDependencies": {
"@types/node": "^26.0.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/node": "^26.3.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"@types/sanitize-html": "^2.16.1",
"@types/shell-quote": "^1.7.5",
"@vitejs/plugin-react": "^6.0.3",
"@vitejs/plugin-react": "^6.1.0",
"dotenv": "^17.4.2",
"postcss": "^8.5.16",
"postcss": "^8.5.26",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"sass-embedded": "^1.100.0",
"sass-embedded": "^1.103.1",
"typescript": "^7.0.2",
"vite": "^8.1.2"
"vite": "^8.2.2"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
+2 -2
View File
@@ -120,10 +120,10 @@ export declare function KomodoClient(url: string, options: InitOptions): {
*/
execute_and_poll: <T extends ExecuteRequest["type"], Req extends Extract<ExecuteRequest, {
type: T;
}>>(type: T, params: Req["params"]) => Promise<Update | (Update | {
}>>(type: T, params: Req["params"]) => Promise<(Update | {
status: "Err";
data: import("./types.js").BatchExecutionResponseItemErr;
})[]>;
})[] | Update>;
/**
* Poll an Update (returned by the `execute` calls) until the `status` is `Complete`.
* https://docs.rs/komodo_client/latest/komodo_client/entities/update/struct.Update.html#structfield.status.
@@ -86,6 +86,7 @@ export default function ContainerSelector({
<Group gap="xs" ml="sm" wrap="nowrap">
{clearable && (
<ActionIcon
component="div"
size="sm"
variant="filled"
color="red"
@@ -93,7 +94,12 @@ export default function ContainerSelector({
e.stopPropagation();
onSelect?.("");
}}
disabled={disabled || !selected}
data-disabled={disabled || !selected || undefined}
style={
disabled || !selected
? { pointerEvents: "none" }
: undefined
}
>
<ICONS.Clear size="0.8rem" />
</ActionIcon>
@@ -94,7 +94,6 @@ export default function ContainersSection({
columns={[
{
accessorKey: "name",
size: 260,
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
@@ -108,7 +107,6 @@ export default function ContainersSection({
},
{
accessorKey: "state",
size: 160,
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
@@ -124,7 +122,6 @@ export default function ContainersSection({
},
{
accessorKey: "image",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
@@ -139,7 +136,6 @@ export default function ContainersSection({
},
{
accessorKey: "networks.0",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Networks" />
),
@@ -167,8 +163,7 @@ export default function ContainersSection({
},
{
accessorKey: "ports.0",
size: 200,
sortingFn: (a, b) => {
sortFn: (a, b) => {
const getMinHostPort = (row: typeof a) => {
const ports = row.original.ports ?? [];
if (!ports.length) return Number.POSITIVE_INFINITY;
@@ -197,7 +192,6 @@ export default function ContainersSection({
},
{
accessorKey: "volumes.0",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Volumes" />
),
+8 -25
View File
@@ -1,5 +1,8 @@
import { useRead } from "@/lib/hooks";
import { Group, Pagination } from "@mantine/core";
import {
ListPaginationProps,
ListPagination as MoghListPagination,
} from "mogh_ui";
/**
* The server side default page size on `List<Resource>` calls,
@@ -18,29 +21,9 @@ export function usePageSize() {
* Only renders when needed, ie the results fill the page
* or the user is past the first page.
*/
export default function ListPagination({
page,
setPage,
count,
}: {
page: number;
setPage: (page: number) => void;
count: number;
}) {
export default function ListPagination(
props: Omit<ListPaginationProps, "pageSize">,
) {
const pageSize = usePageSize();
if (count < pageSize && page === 0) return null;
return (
<Pagination.Root
total={count >= pageSize ? page + 2 : page + 1}
value={page + 1}
onChange={(page) => setPage(page - 1)}
>
<Group gap="0.2rem" justify="center">
<Pagination.First />
<Pagination.Previous />
<Pagination.Items />
<Pagination.Next />
</Group>
</Pagination.Root>
);
return <MoghListPagination {...props} pageSize={pageSize} />;
}
@@ -88,7 +88,6 @@ export default function ConfigMaintenanceWindows({
<Text>{row.original.name}</Text>
</Group>
),
size: 200,
},
{
accessorKey: "schedule_type",
@@ -98,7 +97,6 @@ export default function ConfigMaintenanceWindows({
cell: ({ row }) => (
<ScheduleDescription window={row.original} />
),
size: 150,
},
{
accessorKey: "start_time",
@@ -106,7 +104,6 @@ export default function ConfigMaintenanceWindows({
<SortableHeader column={column} title="Start Time" />
),
cell: ({ row }) => fmtMaintenanceWindowTime(row.original),
size: 180,
},
{
accessorKey: "duration_minutes",
@@ -114,7 +111,6 @@ export default function ConfigMaintenanceWindows({
<SortableHeader column={column} title="Duration" />
),
accessorFn: (row) => `${row.duration_minutes} min`,
size: 100,
},
{
accessorKey: "enabled",
@@ -128,7 +124,6 @@ export default function ConfigMaintenanceWindows({
disabled={disabled}
/>
),
size: 120,
},
{
id: "actions",
@@ -152,7 +147,6 @@ export default function ConfigMaintenanceWindows({
</ActionIcon>
</Group>
),
size: 100,
},
]}
/>
@@ -102,7 +102,6 @@ export default function BasePermissionsSection({
columns={[
{
accessorKey: "type",
size: 150,
header: ({ column }) => (
<SortableHeader column={column} title="Resource Type" />
),
@@ -119,8 +118,7 @@ export default function BasePermissionsSection({
},
{
accessorKey: "level",
size: 150,
sortingFn: (a, b) =>
sortFn: (a, b) =>
levelSortingFn(a.original.level, b.original.level),
header: ({ column }) => (
<SortableHeader column={column} title="Level" />
@@ -143,7 +141,6 @@ export default function BasePermissionsSection({
},
{
header: "Specific",
size: 300,
cell: ({ row }) => {
return (
<SpecificPermissionSelector
@@ -53,7 +53,6 @@ export default function SpecificPermissionsSection({
columns={[
{
accessorKey: "resource_target.type",
size: 150,
header: ({ column }) => (
<SortableHeader column={column} title="Resource" />
),
@@ -72,7 +71,6 @@ export default function SpecificPermissionsSection({
},
{
accessorKey: "resource_target",
size: 250,
header: ({ column }) => (
<SortableHeader column={column} title="Target" />
),
@@ -91,8 +89,7 @@ export default function SpecificPermissionsSection({
},
{
accessorKey: "level",
size: 150,
sortingFn: (a, b) =>
sortFn: (a, b) =>
levelSortingFn(a.original.level, b.original.level),
header: ({ column }) => (
<SortableHeader column={column} title="Level" />
@@ -115,7 +112,6 @@ export default function SpecificPermissionsSection({
},
{
header: "Specific",
size: 300,
cell: ({ row: { original: permission } }) => {
return (
<SpecificPermissionSelector
+7 -1
View File
@@ -101,6 +101,7 @@ export default function StackServiceSelector({
<Group gap="xs" ml="sm" wrap="nowrap">
{clearable && (
<ActionIcon
component="div"
size="sm"
variant="filled"
color="red"
@@ -108,7 +109,12 @@ export default function StackServiceSelector({
e.stopPropagation();
onSelect?.("");
}}
disabled={disabled || !selected}
data-disabled={disabled || !selected || undefined}
style={
disabled || !selected
? { pointerEvents: "none" }
: undefined
}
>
<ICONS.Clear size="0.8rem" />
</ActionIcon>
@@ -66,7 +66,6 @@ export default function SwarmServicesSection({
name={row.original.Name}
/>
),
size: 200,
},
{
accessorKey: "ID",
@@ -74,7 +73,6 @@ export default function SwarmServicesSection({
<SortableHeader column={column} title="ID" />
),
cell: ({ row }) => row.original.ID ?? "Unknown",
size: 200,
},
{
accessorKey: "UpdatedAt",
@@ -85,7 +83,6 @@ export default function SwarmServicesSection({
row.original.UpdatedAt
? new Date(row.original.UpdatedAt).toLocaleString()
: "Unknown",
size: 200,
},
{
accessorKey: "CreatedAt",
@@ -96,7 +93,6 @@ export default function SwarmServicesSection({
row.original.CreatedAt
? new Date(row.original.CreatedAt).toLocaleString()
: "Unknown",
size: 200,
},
]}
/>
@@ -86,7 +86,6 @@ export default function SwarmTasksSection({
name={row.original.ID}
/>
),
size: 150,
},
{
accessorKey: "node.Hostname",
@@ -101,7 +100,6 @@ export default function SwarmTasksSection({
name={row.original.node?.Hostname}
/>
),
size: 200,
},
{
accessorKey: "service.Name",
@@ -116,7 +114,6 @@ export default function SwarmTasksSection({
name={row.original.service?.Name}
/>
),
size: 200,
},
{
accessorKey: "State",
@@ -157,7 +154,6 @@ export default function SwarmTasksSection({
row.original.UpdatedAt
? new Date(row.original.UpdatedAt).toLocaleString()
: "Unknown",
size: 200,
},
// {
// accessorKey: "CreatedAt",
+8 -158
View File
@@ -1,169 +1,19 @@
import { Box, useComputedColorScheme } from "@mantine/core";
import { useEffect, useMemo, useRef } from "react";
import { ITheme } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { useXTerm, UseXTermProps } from "react-xtermjs";
import { TerminalCallbacks } from "komodo_client";
import { lazy, Suspense } from "react";
const LIGHT_THEME: ITheme = {
background: "#EDEEEF",
foreground: "#000000",
cursor: "#000000",
selectionBackground: "#C8D9FA",
scrollbarSliderBackground: "#D3D3D3",
};
// The implementation pulls in @xterm / react-xtermjs,
// so it is loaded lazily to keep them out of the entry chunk.
const TerminalInner = lazy(() => import("./xterm"));
const DARK_THEME: ITheme = {
background: "#15171B",
foreground: "#FFFFFF",
cursor: "#FFFFFF",
selectionBackground: "#B4BFD6",
scrollbarSliderBackground: "#232528",
};
export default function Terminal({
makeWs,
selected,
_reconnect,
_clear,
}: {
export default function Terminal(props: {
makeWs: (callbacks: TerminalCallbacks) => WebSocket | undefined;
selected: boolean;
_reconnect: boolean;
_clear?: boolean;
}) {
const currentTheme = useComputedColorScheme();
const theme = currentTheme === "dark" ? DARK_THEME : LIGHT_THEME;
const wsRef = useRef<WebSocket | null>(null);
const fitRef = useRef<FitAddon>(new FitAddon());
const resize = () => {
fitRef.current.fit();
if (term) {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
const json = JSON.stringify({
rows: term.rows,
cols: term.cols,
});
const buf = new Uint8Array(json.length + 1);
for (let i = 0; i < json.length; i++) buf[i] = json.charCodeAt(i);
buf[json.length] = 0xff; // resize postfix
wsRef.current.send(buf);
}
term.focus();
}
};
const onStdin = (data: string) => {
// This is data user writes to stdin
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
const buf = new Uint8Array(data.length + 1);
for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i);
buf[data.length] = 0x01; // forward data postfix
wsRef.current.send(buf);
};
useEffect(resize, [selected]);
const params: UseXTermProps = useMemo(
() => ({
options: {
convertEol: false,
cursorBlink: true,
cursorStyle: "block",
fontFamily: "monospace",
scrollback: 5000,
// This is handled in ws on_message handler
scrollOnUserInput: false,
theme,
},
listeners: {
onResize: resize,
onData: onStdin,
},
addons: [fitRef.current],
}),
[theme],
);
const { instance: term, ref: termRef } = useXTerm(params);
const viewport = (term as any)?._core?.viewport?._viewportElement as
| HTMLDivElement
| undefined;
useEffect(() => {
if (!term || !viewport) return;
let delta = 0;
term.attachCustomWheelEventHandler((e) => {
e.preventDefault();
// This is used to make touchpad and mousewheel more similar
delta += Math.sign(e.deltaY) * Math.sqrt(Math.abs(e.deltaY)) * 20;
return false;
});
const int = setInterval(() => {
if (Math.abs(delta) < 1) return;
viewport.scrollTop += delta;
delta = 0;
}, 100);
return () => clearInterval(int);
}, [term, termRef.current]);
useEffect(() => {
if (!selected || !term) return;
term.clear();
let debounce = -1;
const callbacks: TerminalCallbacks = {
on_login: () => {
// console.log("Logged into terminal");
},
on_open: resize,
on_message: (e: MessageEvent<any>) => {
term.write(new Uint8Array(e.data as ArrayBuffer), () => {
if (viewport) {
viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;
}
clearTimeout(debounce);
debounce = setTimeout(() => {
if (!viewport) return;
viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;
}, 500) as any;
});
},
on_close: () => {
term.writeln("\r\n\x1b[33m[connection closed]\x1b[0m");
},
};
const ws = makeWs(callbacks);
if (!ws) return;
wsRef.current = ws;
return () => {
ws.close();
wsRef.current = null;
};
}, [term, viewport, makeWs, selected, _reconnect]);
useEffect(() => term?.clear(), [_clear]);
return (
<Box
p="md"
className="bordered-light"
bdrs="md"
style={{
display: selected ? undefined : "none",
}}
>
<Box ref={termRef} h="max(200px, calc(100vh - 320px))" />
</Box>
<Suspense fallback={null}>
<TerminalInner {...props} />
</Suspense>
);
}
+169
View File
@@ -0,0 +1,169 @@
import { Box, useComputedColorScheme } from "@mantine/core";
import { useEffect, useMemo, useRef } from "react";
import { ITheme } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { useXTerm, UseXTermProps } from "react-xtermjs";
import { TerminalCallbacks } from "komodo_client";
const LIGHT_THEME: ITheme = {
background: "#EDEEEF",
foreground: "#000000",
cursor: "#000000",
selectionBackground: "#C8D9FA",
scrollbarSliderBackground: "#D3D3D3",
};
const DARK_THEME: ITheme = {
background: "#15171B",
foreground: "#FFFFFF",
cursor: "#FFFFFF",
selectionBackground: "#B4BFD6",
scrollbarSliderBackground: "#232528",
};
export default function Terminal({
makeWs,
selected,
_reconnect,
_clear,
}: {
makeWs: (callbacks: TerminalCallbacks) => WebSocket | undefined;
selected: boolean;
_reconnect: boolean;
_clear?: boolean;
}) {
const currentTheme = useComputedColorScheme();
const theme = currentTheme === "dark" ? DARK_THEME : LIGHT_THEME;
const wsRef = useRef<WebSocket | null>(null);
const fitRef = useRef<FitAddon>(new FitAddon());
const resize = () => {
fitRef.current.fit();
if (term) {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
const json = JSON.stringify({
rows: term.rows,
cols: term.cols,
});
const buf = new Uint8Array(json.length + 1);
for (let i = 0; i < json.length; i++) buf[i] = json.charCodeAt(i);
buf[json.length] = 0xff; // resize postfix
wsRef.current.send(buf);
}
term.focus();
}
};
const onStdin = (data: string) => {
// This is data user writes to stdin
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
const buf = new Uint8Array(data.length + 1);
for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i);
buf[data.length] = 0x01; // forward data postfix
wsRef.current.send(buf);
};
useEffect(resize, [selected]);
const params: UseXTermProps = useMemo(
() => ({
options: {
convertEol: false,
cursorBlink: true,
cursorStyle: "block",
fontFamily: "monospace",
scrollback: 5000,
// This is handled in ws on_message handler
scrollOnUserInput: false,
theme,
},
listeners: {
onResize: resize,
onData: onStdin,
},
addons: [fitRef.current],
}),
[theme],
);
const { instance: term, ref: termRef } = useXTerm(params);
const viewport = (term as any)?._core?.viewport?._viewportElement as
| HTMLDivElement
| undefined;
useEffect(() => {
if (!term || !viewport) return;
let delta = 0;
term.attachCustomWheelEventHandler((e) => {
e.preventDefault();
// This is used to make touchpad and mousewheel more similar
delta += Math.sign(e.deltaY) * Math.sqrt(Math.abs(e.deltaY)) * 20;
return false;
});
const int = setInterval(() => {
if (Math.abs(delta) < 1) return;
viewport.scrollTop += delta;
delta = 0;
}, 100);
return () => clearInterval(int);
}, [term, termRef.current]);
useEffect(() => {
if (!selected || !term) return;
term.clear();
let debounce = -1;
const callbacks: TerminalCallbacks = {
on_login: () => {
// console.log("Logged into terminal");
},
on_open: resize,
on_message: (e: MessageEvent<any>) => {
term.write(new Uint8Array(e.data as ArrayBuffer), () => {
if (viewport) {
viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;
}
clearTimeout(debounce);
debounce = setTimeout(() => {
if (!viewport) return;
viewport.scrollTop = viewport.scrollHeight - viewport.clientHeight;
}, 500) as any;
});
},
on_close: () => {
term.writeln("\r\n\x1b[33m[connection closed]\x1b[0m");
},
};
const ws = makeWs(callbacks);
if (!ws) return;
wsRef.current = ws;
return () => {
ws.close();
wsRef.current = null;
};
}, [term, viewport, makeWs, selected, _reconnect]);
useEffect(() => term?.clear(), [_clear]);
return (
<Box
p="md"
className="bordered-light"
bdrs="md"
style={{
display: selected ? undefined : "none",
}}
>
<Box ref={termRef} h="max(200px, calc(100vh - 320px))" />
</Box>
);
}
+6 -2
View File
@@ -2,7 +2,7 @@ import { useUser } from "@/lib/hooks";
import { ICONS } from "@/lib/icons";
import { ConfirmButton } from "mogh_ui";
import { ConfirmModal } from "mogh_ui";
import { DataTable } from "mogh_ui";
import { DataTable, DataTableFeatures } from "mogh_ui";
import { StatusBadge } from "mogh_ui";
import { Badge, BoxProps } from "@mantine/core";
import { ColumnDef } from "@tanstack/react-table";
@@ -27,7 +27,11 @@ export default function UserTable({
}: UserTableProps) {
const user = useUser().data;
const nav = useNavigate();
const columns: ColumnDef<Types.User, "User" | "Admin" | "Super Admin">[] = [
const columns: ColumnDef<
DataTableFeatures,
Types.User,
"User" | "Admin" | "Super Admin"
>[] = [
{ header: "Username", accessorKey: "username" },
{
header: "Type",
+7
View File
@@ -3,6 +3,7 @@ import { Types } from "komodo_client";
import sanitizeHtml from "sanitize-html";
import ConvertAnsiToHtml from "ansi-to-html";
import { RowSelectionState } from "@tanstack/react-table";
import { Crumb } from "mogh_ui";
export const EXECUTION_ACTION_STATE_REQUERY_MS = 500;
@@ -320,3 +321,9 @@ export function parseVersion(version: string): Types.Version {
patch,
};
}
/** Crumb linking to the list page for a resource type, eg. `Servers` -> /servers */
export function resourceTypeCrumb(type: UsableResource): Crumb {
const name = type === "ResourceSync" ? "Resource Sync" : type;
return { label: `${name}s`, to: `/${usableResourcePath(type)}` };
}
+4 -3
View File
@@ -6,7 +6,6 @@ import { Router } from "@/router";
import { setAuthUrl, ThemeProvider } from "mogh_ui";
import { themeAdditionalColors } from "@/lib/color";
import { Notifications } from "@mantine/notifications";
import initMonaco from "@/monaco";
import "@mantine/core/styles.css";
// ‼️ import extra package styles after core package styles
@@ -17,10 +16,12 @@ import "./index.scss";
// Import mogh_ui scss
import "mogh_ui/index.scss";
initMonaco();
// Loaded dynamically to keep monaco-editor / monaco-yaml out of the entry chunk.
// Doesn't need to be awaited - applies in background when ready.
import("@/monaco").then(({ default: initMonaco }) => initMonaco());
export const KOMODO_BASE_URL =
import.meta.env.VITE_KOMODO_HOST ?? location.origin;
import.meta.env.VITE_KOMODO_HOST || location.origin;
export const UPDATE_WS_URL =
KOMODO_BASE_URL.replace("http", "ws") + "/ws/update";
const client = new QueryClient({
+2 -9
View File
@@ -96,7 +96,6 @@ export default function Containers() {
{
id: "Name",
accessorKey: "name",
size: 260,
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
@@ -111,8 +110,7 @@ export default function Containers() {
{
id: "Server",
accessorKey: "server_id",
size: 200,
sortingFn: (a, b) => {
sortFn: (a, b) => {
const sa = a.original.server_name;
const sb = b.original.server_name;
@@ -134,7 +132,6 @@ export default function Containers() {
{
id: "State",
accessorKey: "state",
size: 160,
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
@@ -151,7 +148,6 @@ export default function Containers() {
{
id: "Image",
accessorKey: "image",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
@@ -167,7 +163,6 @@ export default function Containers() {
{
id: "Networks",
accessorKey: "networks.0",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Networks" />
),
@@ -196,8 +191,7 @@ export default function Containers() {
{
id: "Ports",
accessorKey: "ports.0",
size: 200,
sortingFn: (a, b) => {
sortFn: (a, b) => {
const getMinHostPort = (row: typeof a) => {
const ports = row.original.ports ?? [];
if (!ports.length) return Number.POSITIVE_INFINITY;
@@ -227,7 +221,6 @@ export default function Containers() {
{
id: "Volumes",
accessorKey: "volumes.0",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Volumes" />
),
-2
View File
@@ -176,7 +176,6 @@ function ImageInner({
{
accessorKey: "CreatedBy",
header: "Created By",
size: 400,
},
{
accessorKey: "Created",
@@ -185,7 +184,6 @@ function ImageInner({
fmtDateWithMinutes(
new Date(row.original.Created * 1000),
),
size: 200,
},
]}
/>
-1
View File
@@ -165,7 +165,6 @@ function NetworkInner({
) : (
"Unknown"
),
size: 200,
},
{
accessorKey: "IPv4Address",
+14 -4
View File
@@ -14,13 +14,12 @@ import {
UsableResource,
} from "@/resources";
import { AddResourceTags, ResourceTags } from "@/resources/tags";
import { DividedChildren } from "mogh_ui";
import { Section } from "mogh_ui";
import { DividedChildren, EntityPage } from "mogh_ui";
import { Section, PageBreadcrumbs } from "mogh_ui";
import { Group, Stack, Text } from "@mantine/core";
import { Types } from "komodo_client";
import { Link, useParams } from "react-router-dom";
import { EntityPage } from "mogh_ui";
import { usableResourcePath } from "@/lib/utils";
import { resourceTypeCrumb, usableResourcePath } from "@/lib/utils";
import ResourceDescription from "@/resources/description";
import ResourceNotFound from "@/resources/not-found";
import NewResource from "@/resources/new";
@@ -67,6 +66,17 @@ function ResourceInner({ type, id }: { type: UsableResource; id: string }) {
? "settings"
: usableResourcePath(type))
}
breadcrumbs={
<PageBreadcrumbs
items={[
...(SETTINGS_RESOURCES.includes(type)
? [{ label: "Settings", to: "/settings" }]
: []),
resourceTypeCrumb(type),
{ label: resource.name },
]}
/>
}
actions={
<>
{canCreate && <NewResource type={type} copyId={id} />}
-4
View File
@@ -100,7 +100,6 @@ export default function Schedules() {
}}
columns={[
{
size: 200,
id: "Name",
accessorKey: "name",
header: ({ column }) => (
@@ -114,7 +113,6 @@ export default function Schedules() {
),
},
{
size: 200,
id: "Schedule",
accessorKey: "schedule",
header: ({ column }) => (
@@ -122,7 +120,6 @@ export default function Schedules() {
),
},
{
size: 200,
id: "NextRun",
accessorKey: "next_scheduled_run",
header: ({ column }) => (
@@ -134,7 +131,6 @@ export default function Schedules() {
: "Not Scheduled",
},
{
size: 100,
id: "Enabled",
accessorKey: "enabled",
header: ({ column }) => (
+7 -10
View File
@@ -3,7 +3,12 @@ import TagSelector from "@/components/tags/selector";
import { useInvalidate, useRead, useSetTitle, useWrite } from "@/lib/hooks";
import ResourceSelector from "@/resources/selector";
import { ICONS } from "@/lib/icons";
import { DataTable, fmtDateWithMinutes, SortableHeader } from "mogh_ui";
import {
DataTable,
DataTableFeatures,
fmtDateWithMinutes,
SortableHeader,
} from "mogh_ui";
import { Badge, Group, Switch, TextInput, useMatches } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { ColumnDef } from "@tanstack/react-table";
@@ -29,13 +34,12 @@ export default function SettingsOnboardingKeys() {
xl: "md",
});
const columns: (
| ColumnDef<Types.OnboardingKey, unknown>
| ColumnDef<DataTableFeatures, Types.OnboardingKey, unknown>
| false
| undefined
)[] = useMemo(
() => [
{
size: 150,
accessorKey: "name",
header: ({ column }) => <SortableHeader column={column} title="Name" />,
cell: ({ row }) => (
@@ -54,7 +58,6 @@ export default function SettingsOnboardingKeys() {
),
},
{
size: 150,
accessorKey: "copy_server",
header: "Template",
cell: ({ row }) => (
@@ -69,7 +72,6 @@ export default function SettingsOnboardingKeys() {
),
},
{
size: 200,
accessorKey: "tags",
header: "Tags",
cell: ({ row }) => {
@@ -116,7 +118,6 @@ export default function SettingsOnboardingKeys() {
},
},
{
size: 100,
accessorKey: "privileged",
header: ({ column }) => (
<SortableHeader
@@ -138,7 +139,6 @@ export default function SettingsOnboardingKeys() {
),
},
{
size: 100,
accessorKey: "create_builder",
header: ({ column }) => (
<SortableHeader column={column} title="Create Builder" />
@@ -156,7 +156,6 @@ export default function SettingsOnboardingKeys() {
),
},
{
size: 100,
accessorKey: "enabled",
header: ({ column }) => (
<SortableHeader column={column} title="Enabled" />
@@ -174,7 +173,6 @@ export default function SettingsOnboardingKeys() {
),
},
{
size: 150,
accessorKey: "expires",
header: ({ column }) => (
<SortableHeader column={column} title="Expires" />
@@ -196,7 +194,6 @@ export default function SettingsOnboardingKeys() {
),
},
{
size: 100,
accessorKey: "public_key",
header: "Delete",
cell: ({ row }) => (
@@ -83,7 +83,6 @@ function Providers({ type }: { type: "GitProvider" | "ImageRegistry" }) {
columns={[
{
accessorKey: "domain",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Domain" />
),
@@ -141,7 +140,6 @@ function Providers({ type }: { type: "GitProvider" | "ImageRegistry" }) {
},
{
accessorKey: "username",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Username" />
),
@@ -180,7 +178,6 @@ function Providers({ type }: { type: "GitProvider" | "ImageRegistry" }) {
},
{
accessorKey: "token",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Token" />
),
@@ -220,7 +217,6 @@ function Providers({ type }: { type: "GitProvider" | "ImageRegistry" }) {
},
{
header: "Delete",
maxSize: 200,
cell: ({ row }) => (
<DeleteProviderAccount
type={type}
+10 -14
View File
@@ -1,6 +1,5 @@
import NewTag from "@/components/tags/new";
import Tag from "@/components/tags/tag";
import UserAvatar from "@/components/user-avatar";
import { useRead, useSetTitle, useUser } from "@/lib/hooks";
import { DataTable } from "mogh_ui";
import { Group, Stack } from "@mantine/core";
@@ -30,7 +29,6 @@ export default function SettingsTags() {
columns={[
{
header: "Name",
size: 200,
accessorKey: "name",
cell: ({ row }) => {
return <Tag tag={row.original} size="md" fz="md" />;
@@ -38,7 +36,6 @@ export default function SettingsTags() {
},
{
header: "Color",
size: 200,
cell: ({ row }) => (
<TagColorSelector
tagId={row.original._id?.$oid!}
@@ -47,19 +44,18 @@ export default function SettingsTags() {
/>
),
},
{
header: "Owner",
size: 200,
cell: ({ row }) =>
row.original.owner ? (
<UserAvatar userId={row.original.owner} fz="md" />
) : (
"Unknown"
),
},
// {
// header: "Owner",
// size: 200,
// cell: ({ row }) =>
// row.original.owner ? (
// <UserAvatar userId={row.original.owner} fz="md" />
// ) : (
// "Unknown"
// ),
// },
{
header: "Delete",
size: 200,
cell: ({ row }) => (
<DeleteTag
tagId={row.original._id!.$oid}
@@ -67,7 +67,6 @@ export default function SettingsVariables() {
columns={[
{
accessorKey: "name",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
@@ -79,7 +78,6 @@ export default function SettingsVariables() {
},
{
accessorKey: "value",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Value" />
),
@@ -117,7 +115,6 @@ export default function SettingsVariables() {
},
{
accessorKey: "description",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Description" />
),
@@ -153,7 +150,6 @@ export default function SettingsVariables() {
},
{
header: "Secret",
size: 100,
cell: ({ row }) => (
<Switch
checked={row.original.is_secret}
@@ -169,7 +165,6 @@ export default function SettingsVariables() {
},
{
header: "Delete",
size: 200,
cell: ({ row }) => (
<DeleteVariable name={row.original.name} disabled={disabled} />
),
+1 -1
View File
@@ -492,7 +492,7 @@ function MultiServerStatChart({
fontSize: 12,
}}
labelStyle={{ color: "var(--mantine-color-dimmed)" }}
labelFormatter={(label) => new Date(label).toLocaleString()}
labelFormatter={(label) => new Date(Number(label)).toLocaleString()}
// List the busiest server first.
itemSorter={(item) => -Number(item.value)}
formatter={(value, name) => {
-2
View File
@@ -123,7 +123,6 @@ export default function Terminals() {
),
},
{
size: 200,
id: "Target",
accessorKey: "target",
header: ({ column }) => (
@@ -146,7 +145,6 @@ export default function Terminals() {
),
},
{
size: 100,
id: "Size",
accessorKey: "stored_size_kb",
header: ({ column }) => (
+19 -6
View File
@@ -1,11 +1,14 @@
import UserTable from "@/components/user/table";
import { useInvalidate, useRead, useUser, useWrite } from "@/lib/hooks";
import { ICONS } from "@/lib/icons";
import { DividedChildren } from "mogh_ui";
import { EntityHeader } from "mogh_ui";
import { EntityPage } from "mogh_ui";
import { PageGuard } from "mogh_ui";
import { Section } from "mogh_ui";
import {
DividedChildren,
EntityHeader,
EntityPage,
PageBreadcrumbs,
PageGuard,
Section,
} from "mogh_ui";
import { Group, Stack, Text } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useParams } from "react-router-dom";
@@ -66,7 +69,17 @@ export default function UserGroup() {
: undefined
}
>
<EntityPage backTo="/settings">
<EntityPage
backTo="/settings"
breadcrumbs={
<PageBreadcrumbs
items={[
{ label: "Settings", to: "/settings" },
{ label: group?.name },
]}
/>
}
>
<Stack gap="md" pb="md" className="bordered-light" bdrs="md">
<EntityHeader
name={group?.name}
+20 -12
View File
@@ -1,9 +1,12 @@
import { useInvalidate, useRead, useUser, useWrite } from "@/lib/hooks";
import { ConfirmButton } from "mogh_ui";
import { EntityPage } from "mogh_ui";
import { LabelledSwitch } from "mogh_ui";
import { PageGuard } from "mogh_ui";
import { Section } from "mogh_ui";
import {
ConfirmButton,
EntityPage,
LabelledSwitch,
PageGuard,
PageBreadcrumbs,
Section,
} from "mogh_ui";
import { Group, Stack } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { UserCheck, UserMinus } from "lucide-react";
@@ -48,17 +51,22 @@ export default function User() {
: undefined
}
>
<EntityPage backTo="/settings">
<EntityPage
backTo="/settings"
breadcrumbs={
<PageBreadcrumbs
items={[
{ label: "Settings", to: "/settings" },
{ label: user?.username },
]}
/>
}
>
<UserHeader user={user} />
<Stack mt="lg" gap="xl">
{user?._id?.$oid !== adminUser?._id?.$oid &&
(!user?.admin || (!user.super_admin && adminUser?.super_admin)) && (
<Section
title="User Permissions"
titleFz="h3"
withBorder
>
<Section title="User Permissions" titleFz="h3" withBorder>
<Group>
<ConfirmButton
icon={
+1 -1
View File
@@ -61,7 +61,7 @@ export default function ActionTable({
header: ({ column }) => (
<SortableHeader column={column} title="Next Run" />
),
sortingFn: (a, b) => {
sortFn: (a, b) => {
const sa = a.original.info.next_scheduled_run;
const sb = b.original.info.next_scheduled_run;
-4
View File
@@ -52,7 +52,6 @@ export default function BuildTable({
id: "Name",
accessorKey: "name",
cell: ({ row }) => <ResourceLink type="Build" id={row.original.id} />,
size: 200,
},
{
header: ({ column }) => (
@@ -61,12 +60,10 @@ export default function BuildTable({
id: "Source",
accessorKey: "info.repo",
cell: ({ row }) => <FileSource info={row.original.info} />,
size: 200,
},
{
header: "Version",
accessorFn: ({ info }) => fmtVersion(info.version),
size: 120,
},
{
id: "State",
@@ -75,7 +72,6 @@ export default function BuildTable({
<SortableHeader column={column} title="State" />
),
cell: ({ row }) => <BuildComponents.State id={row.original.id} />,
size: 120,
},
{
header: "Tags",
+1 -1
View File
@@ -10,7 +10,7 @@ export default function BuilderInstanceType({ id }: { id: string }) {
return (
<DividedChildren wrap="nowrap" gap="xs">
{info.instance_type?.split(",").map((id) => (
<ResourceLink type="Server" id={id.trim()} />
<ResourceLink key={id.trim()} type="Server" id={id.trim()} />
))}
</DividedChildren>
);
+1 -5
View File
@@ -57,7 +57,6 @@ export default function DeploymentTable({
<DeploymentUpdateAvailable id={row.original.id} small />
</Group>
),
size: 200,
},
{
id: "Image",
@@ -72,7 +71,6 @@ export default function DeploymentTable({
},
},
}) => <Image buildId={build_id} image={image} />,
size: 200,
},
{
header: ({ column }) => (
@@ -80,7 +78,7 @@ export default function DeploymentTable({
),
id: "Host",
accessorKey: "info.server_id",
sortingFn: (a, b) => {
sortFn: (a, b) => {
const name_a = a.original.info.swarm_id
? a.original.info.swarm_name
: a.original.info.server_name;
@@ -102,7 +100,6 @@ export default function DeploymentTable({
) : (
<ResourceLink type="Server" id={row.original.info.server_id} />
),
size: 200,
},
{
id: "State",
@@ -113,7 +110,6 @@ export default function DeploymentTable({
cell: ({ row }) => (
<DeploymentComponents.State id={row.original.id} />
),
size: 120,
},
{
header: "Tags",
@@ -131,7 +131,6 @@ export default function ProcedureStage({
columns={[
{
header: "Execution",
size: 250,
cell: ({ row: { original, index } }) => (
<ProcedureExecutionSelector
disabled={disabled}
@@ -160,7 +159,6 @@ export default function ProcedureStage({
},
{
header: "Target",
size: 250,
cell: ({
row: {
original: {
@@ -195,7 +193,6 @@ export default function ProcedureStage({
},
{
header: "Add / Remove",
size: 150,
cell: ({ row: { index } }) => (
<Group>
<ActionIcon
@@ -232,7 +229,6 @@ export default function ProcedureStage({
},
{
header: "Enabled",
size: 100,
cell: ({
row: {
original: { enabled },
+1 -1
View File
@@ -67,7 +67,7 @@ export default function ProcedureTable({
header: ({ column }) => (
<SortableHeader column={column} title="Next Run" />
),
sortingFn: (a, b) => {
sortFn: (a, b) => {
const sa = a.original.info.next_scheduled_run;
const sb = b.original.info.next_scheduled_run;
-4
View File
@@ -51,7 +51,6 @@ export default function RepoTable({
id: "Name",
accessorKey: "name",
cell: ({ row }) => <ResourceLink type="Repo" id={row.original.id} />,
size: 200,
},
{
header: ({ column }) => (
@@ -65,7 +64,6 @@ export default function RepoTable({
link={row.original.info.repo_link}
/>
),
size: 200,
},
{
header: ({ column }) => (
@@ -73,7 +71,6 @@ export default function RepoTable({
),
id: "Branch",
accessorKey: "info.branch",
size: 200,
},
{
header: ({ column }) => (
@@ -82,7 +79,6 @@ export default function RepoTable({
id: "State",
accessorKey: "info.state",
cell: ({ row }) => <RepoComponents.State id={row.original.id} />,
size: 120,
},
{
header: "Tags",
+7 -1
View File
@@ -123,6 +123,7 @@ export default function ResourceSelector({
<Group gap="xs" ml="sm" wrap="nowrap">
{clearable && (
<ActionIcon
component="div"
size="sm"
variant="filled"
color="red"
@@ -130,7 +131,12 @@ export default function ResourceSelector({
e.stopPropagation();
onSelect?.("");
}}
disabled={disabled || !selected}
data-disabled={disabled || !selected || undefined}
style={
disabled || !selected
? { pointerEvents: "none" }
: undefined
}
>
<ICONS.Clear size="0.8rem" />
</ActionIcon>
@@ -63,7 +63,6 @@ export default function ServerImages({
}
/>
),
size: 200,
},
{
accessorKey: "id",
@@ -85,7 +85,6 @@ export default function ServerNetworks({
/>
</div>
),
size: 300,
},
{
accessorKey: "driver",
@@ -62,7 +62,6 @@ export default function ServerVolumes({
}
/>
),
size: 200,
},
{
accessorKey: "driver",
@@ -58,7 +58,6 @@ export default function ServerContainerStats({ id }: { id: string }) {
columns={[
{
accessorKey: "name",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Name" />
),
@@ -72,14 +71,12 @@ export default function ServerContainerStats({ id }: { id: string }) {
},
{
accessorKey: "stats.cpu_perc",
size: 100,
header: ({ column }) => (
<SortableHeader column={column} title="CPU" />
),
},
{
accessorKey: "stats.mem_perc",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Memory" />
),
@@ -94,21 +91,18 @@ export default function ServerContainerStats({ id }: { id: string }) {
},
{
accessorKey: "stats.net_io",
size: 150,
header: ({ column }) => (
<SortableHeader column={column} title="Net I/O" />
),
},
{
accessorKey: "stats.block_io",
size: 150,
header: ({ column }) => (
<SortableHeader column={column} title="Block I/O" />
),
},
{
accessorKey: "stats.pids",
size: 100,
header: ({ column }) => (
<SortableHeader column={column} title="PIDs" />
),
+1 -1
View File
@@ -335,7 +335,7 @@ function StatChart({
fontSize: 12,
}}
labelStyle={{ color: "var(--mantine-color-dimmed)" }}
labelFormatter={(label) => new Date(label).toLocaleString()}
labelFormatter={(label) => new Date(Number(label)).toLocaleString()}
// Memory tooltip lists top-of-stack first, matching the chart.
itemSorter={
isMemory
+7 -3
View File
@@ -1,4 +1,4 @@
import { ReactNode } from "react";
import { lazy, ReactNode, Suspense } from "react";
import { usePermissions, useRead } from "@/lib/hooks";
import { Types } from "komodo_client";
import { Section } from "mogh_ui";
@@ -6,10 +6,12 @@ import ServerProcesses from "./processes";
import ServerContainerStats from "./containers";
import ServerDisks from "./disks";
import ServerCurrentStats from "./current";
import ServerHistoricalStats from "./historical";
import ServerSystemInfo from "./system-info";
import { useIsServerAvailable } from "../hooks";
// Loaded lazily to keep recharts out of the entry chunk.
const ServerHistoricalStats = lazy(() => import("./historical"));
export default function ServerStats({
id,
titleOther,
@@ -35,7 +37,9 @@ export default function ServerStats({
<ServerCurrentStats id={id} stats={stats} />
<ServerHistoricalStats id={id} />
<Suspense fallback={null}>
<ServerHistoricalStats id={id} />
</Suspense>
<ServerContainerStats id={id} />
+1 -6
View File
@@ -53,7 +53,6 @@ export default function StandardServerTable({
}}
columns={[
{
size: 250,
id: "Name",
accessorKey: "name",
header: ({ column }) => (
@@ -64,12 +63,11 @@ export default function StandardServerTable({
),
},
{
size: 100,
accessorKey: "id",
// The resource count is computed on the client,
// it cannot be sorted server side.
enableSorting: !onServerSort,
sortingFn: (a, b) => {
sortFn: (a, b) => {
const sa = resourcesCount(a.original.id);
const sb = resourcesCount(b.original.id);
@@ -89,7 +87,6 @@ export default function StandardServerTable({
},
},
{
size: 200,
id: "Region",
accessorKey: "info.region",
header: ({ column }) => (
@@ -97,7 +94,6 @@ export default function StandardServerTable({
),
},
{
size: 150,
id: "Version",
accessorKey: "info.version",
header: ({ column }) => (
@@ -106,7 +102,6 @@ export default function StandardServerTable({
// cell: ({ row }) => <ServerVersion id={row.original.id} />,
},
{
size: 150,
id: "State",
accessorKey: "info.state",
header: ({ column }) => (
-10
View File
@@ -55,7 +55,6 @@ function StackServicesSwarm({
columns={[
{
accessorKey: "service",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Service" />
),
@@ -65,7 +64,6 @@ function StackServicesSwarm({
},
{
accessorKey: "swarm_service.State",
size: 160,
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
@@ -87,14 +85,12 @@ function StackServicesSwarm({
},
{
accessorKey: "swarm_service.Runtime",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Runtime" />
),
},
{
accessorKey: "swarm_service.Image",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
@@ -105,7 +101,6 @@ function StackServicesSwarm({
},
{
accessorKey: "swarm_service.Replicas",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Replicas" />
),
@@ -132,7 +127,6 @@ function StackServicesServer({
columns={[
{
accessorKey: "service",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Service" />
),
@@ -142,7 +136,6 @@ function StackServicesServer({
},
{
accessorKey: "container.state",
size: 160,
header: ({ column }) => (
<SortableHeader column={column} title="State" />
),
@@ -164,7 +157,6 @@ function StackServicesServer({
},
{
accessorKey: "container.image",
size: 300,
header: ({ column }) => (
<SortableHeader column={column} title="Image" />
),
@@ -181,7 +173,6 @@ function StackServicesServer({
},
{
accessorKey: "container.networks.0",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Networks" />
),
@@ -215,7 +206,6 @@ function StackServicesServer({
},
{
accessorKey: "container.ports.0",
size: 200,
header: ({ column }) => (
<SortableHeader column={column} title="Ports" />
),
+1 -5
View File
@@ -59,7 +59,6 @@ export default function StackTable({
</Group>
);
},
size: 200,
},
{
header: ({ column }) => (
@@ -68,7 +67,6 @@ export default function StackTable({
id: "Source",
accessorKey: "info.repo",
cell: ({ row }) => <FileSource info={row.original.info} />,
size: 200,
},
{
header: ({ column }) => (
@@ -76,7 +74,7 @@ export default function StackTable({
),
id: "Host",
accessorKey: "info.server_id",
sortingFn: (a, b) => {
sortFn: (a, b) => {
const name_a = a.original.info.swarm_id
? a.original.info.swarm_name
: a.original.info.server_name;
@@ -98,7 +96,6 @@ export default function StackTable({
) : (
<ResourceLink type="Server" id={row.original.info.server_id} />
),
size: 200,
},
{
id: "State",
@@ -107,7 +104,6 @@ export default function StackTable({
<SortableHeader column={column} title="State" />
),
cell: ({ row }) => <StackComponents.State id={row.original.id} />,
size: 120,
},
{
header: "Tags",
+18 -2
View File
@@ -1,16 +1,17 @@
import { UsableResource } from ".";
import { ResourceComponents, UsableResource } from ".";
import {
EntityHeader,
EntityHeaderProps,
EntityPage,
EntityPageProps,
PageBreadcrumbs,
} from "mogh_ui";
import { ReactNode } from "react";
import { Group, Stack, Text } from "@mantine/core";
import { DividedChildren } from "mogh_ui";
import ResourceLink from "./link";
import ResourceDescription from "./description";
import { usableResourcePath } from "@/lib/utils";
import { resourceTypeCrumb, usableResourcePath } from "@/lib/utils";
import ResourceUpdates from "@/components/updates/resource";
import { usePermissions } from "@/lib/hooks";
import { Section } from "mogh_ui";
@@ -37,6 +38,7 @@ export default function ResourceSubPage({
...headerProps
}: ResourceSubPageProps) {
const { canExecute } = usePermissions({ type: parentType, id: parentId });
const parent = ResourceComponents[parentType].useListItem(parentId);
const Header = (
<Stack justify="space-between">
<Stack gap="md" pb="md" className="bordered-light" bdrs="md">
@@ -56,6 +58,20 @@ export default function ResourceSubPage({
backTo={
pageProps?.backTo ?? `/${usableResourcePath(parentType)}/${parentId}`
}
breadcrumbs={
pageProps?.breadcrumbs ?? (
<PageBreadcrumbs
items={[
resourceTypeCrumb(parentType),
{
label: parent?.name ?? "Unknown",
to: `/${usableResourcePath(parentType)}/${parentId}`,
},
{ label: headerProps.name },
]}
/>
)
}
>
<Stack hiddenFrom="lg" w="100%">
{Header}
@@ -67,7 +67,6 @@ export default function SwarmNodes({
name={row.original.Hostname}
/>
),
size: 200,
},
{
accessorKey: "ID",
@@ -75,7 +74,6 @@ export default function SwarmNodes({
<SortableHeader column={column} title="ID" />
),
cell: ({ row }) => row.original.ID ?? "Unknown",
size: 200,
},
{
accessorKey: "Role",
@@ -149,7 +147,6 @@ export default function SwarmNodes({
row.original.UpdatedAt
? new Date(row.original.UpdatedAt).toLocaleString()
: "Unknown",
size: 200,
},
// {
// accessorKey: "CreatedAt",
@@ -61,7 +61,6 @@ export default function SwarmSecrets({
}
/>
),
size: 200,
},
{
accessorKey: "ID",
@@ -69,7 +68,6 @@ export default function SwarmSecrets({
<SortableHeader column={column} title="ID" />
),
cell: ({ row }) => row.original.ID ?? "Unknown",
size: 200,
},
{
accessorKey: "Driver",
@@ -100,7 +98,6 @@ export default function SwarmSecrets({
row.original.UpdatedAt
? new Date(row.original.UpdatedAt).toLocaleString()
: "Unknown",
size: 200,
},
{
accessorKey: "CreatedAt",
@@ -111,7 +108,6 @@ export default function SwarmSecrets({
row.original.CreatedAt
? new Date(row.original.CreatedAt).toLocaleString()
: "Unknown",
size: 200,
},
]}
/>
-2
View File
@@ -50,7 +50,6 @@ export default function SwarmTable({
id: "Name",
accessorKey: "name",
cell: ({ row }) => <ResourceLink type="Swarm" id={row.original.id} />,
size: 200,
},
{
header: ({ column }) => (
@@ -59,7 +58,6 @@ export default function SwarmTable({
id: "State",
accessorKey: "info.state",
cell: ({ row }) => <SwarmComponents.State id={row.original.id} />,
size: 120,
},
{
header: "Tags",
-4
View File
@@ -52,7 +52,6 @@ export default function ResourceSyncTable({
cell: ({ row }) => (
<ResourceLink type="ResourceSync" id={row.original.id} />
),
size: 200,
},
{
header: ({ column }) => (
@@ -61,7 +60,6 @@ export default function ResourceSyncTable({
id: "Source",
accessorKey: "info.repo",
cell: ({ row }) => <FileSource info={row.original.info} />,
size: 200,
},
{
header: ({ column }) => (
@@ -69,7 +67,6 @@ export default function ResourceSyncTable({
),
id: "Branch",
accessorKey: "info.branch",
size: 200,
},
{
header: ({ column }) => (
@@ -80,7 +77,6 @@ export default function ResourceSyncTable({
cell: ({ row }) => (
<ResourceSyncComponents.State id={row.original.id} />
),
size: 120,
},
{
header: "Tags",
+47 -2
View File
@@ -13,7 +13,7 @@ export default defineConfig({
},
resolve: {
alias: [
{ find: "@", replacement: path.resolve(__dirname, "./src") },
{ find: "@", replacement: path.resolve(import.meta.dirname, "./src") },
// monaco-editor >= 0.53 has an exports map ("./*.js": "./esm/vs/*.js"),
// so legacy deep imports like "monaco-editor/esm/vs/..." no longer
// resolve. monaco-worker-manager (used by monaco-yaml's worker) still
@@ -40,7 +40,52 @@ export default defineConfig({
"react-router-dom",
],
},
optimizeDeps: { exclude: ["mogh_ui"] },
optimizeDeps: {
exclude: ["mogh_ui"],
// path-browserify is a CJS dep of monaco-yaml's yaml.worker. Vite's dep
// scanner doesn't traverse `?worker` graphs, so without this it gets
// served raw ("module is not defined" inside the worker). Force it
// through prebundling to get CJS -> ESM interop.
// @mantine/form and @tanstack/react-table are runtime-imported only via
// the excluded (linked) mogh_ui — app src imports of react-table are
// type-only, which the scanner erases — so they get served raw and their
// nested CJS deps (fast-deep-equal, use-sync-external-store) break with
// "does not provide an export named ...". Prebundling the packages
// inlines those CJS deps with proper interop.
include: ["path-browserify", "@mantine/form", "@tanstack/react-table"],
},
build: {
// The only chunks above the default 500 kB warning limit are
// monaco-editor's core (~2.7 MB) and prettier's typescript parser
// (~900 kB). Both are already async — mogh_ui lazy-loads the editor
// implementation, so they only download when an editor mounts — and
// neither can be split further (each is one static import graph).
// Raise the limit above them so the warning still catches an eager
// chunk regression instead of firing on every build.
chunkSizeWarningLimit: 2800,
rolldownOptions: {
output: {
// Split stable vendor code out of the main chunk so app-code
// changes don't invalidate the browser cache for react/mantine.
codeSplitting: {
groups: [
{
name: "react",
test: /node_modules[\\/](react|react-dom|scheduler|react-router)[\\/]/,
},
{
name: "mantine",
test: /node_modules[\\/](@mantine|@floating-ui|react-transition-group|react-remove-scroll)[\\/]/,
},
{
name: "tanstack",
test: /node_modules[\\/]@tanstack[\\/]/,
},
],
},
},
},
},
css: {
preprocessorOptions: {
scss: {
+448 -422
View File
File diff suppressed because it is too large Load Diff