mirror of
https://github.com/moghtech/komodo.git
synced 2026-11-02 08:29:18 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
780ac68b99 | ||
|
|
cc1c5aa5da |
Generated
+233
-918
File diff suppressed because it is too large
Load Diff
+19
-19
@@ -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
@@ -1,5 +1 @@
|
||||
{
|
||||
"imports": {
|
||||
"@std/toml": "jsr:@std/toml"
|
||||
}
|
||||
}
|
||||
{}
|
||||
+1
-42
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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
@@ -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
@@ -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}"))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
use anyhow::Context;
|
||||
use bollard::query_parameters::ListTasksOptions;
|
||||
use komodo_client::entities::docker::task::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
FROM rust:1.97.1-trixie as builder
|
||||
FROM rust:1.98.0-trixie as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.97.1-trixie as builder
|
||||
FROM rust:1.98.0-trixie as builder
|
||||
WORKDIR /builder
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -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" />
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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" />
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -165,7 +165,6 @@ function NetworkInner({
|
||||
) : (
|
||||
"Unknown"
|
||||
),
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: "IPv4Address",
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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
@@ -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={
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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" />
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user